From 1401c2c4fe4fbbb63c5d5244ef2e79f8b1801e56 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 21 Jan 2026 23:04:43 +0800 Subject: [PATCH 001/386] Render local types in checking error messages This refactors the error reporting in the checking crate to render local types eagerly. This is a pre-requisite for creating a diagnostics crate, which will be used for the LSP's error reporting and integration tests. --- .../compiler_solved/prim_type_error.rs | 6 +- .../checking/src/algorithm/derive.rs | 19 +++--- .../src/algorithm/derive/contravariant.rs | 9 ++- .../checking/src/algorithm/derive/eq1.rs | 5 +- .../checking/src/algorithm/derive/foldable.rs | 9 ++- .../checking/src/algorithm/derive/functor.rs | 9 ++- .../checking/src/algorithm/derive/generic.rs | 6 +- .../checking/src/algorithm/derive/newtype.rs | 10 ++-- .../checking/src/algorithm/derive/tools.rs | 8 ++- .../src/algorithm/derive/traversable.rs | 9 ++- .../checking/src/algorithm/derive/variance.rs | 20 +++---- compiler-core/checking/src/algorithm/state.rs | 23 +++++++- compiler-core/checking/src/algorithm/term.rs | 2 +- .../checking/src/algorithm/term_item.rs | 14 ++--- .../checking/src/algorithm/type_item.rs | 3 +- .../checking/src/algorithm/unification.rs | 6 +- compiler-core/checking/src/error.rs | 32 +++++----- compiler-core/checking/src/lib.rs | 6 +- docs/src/wasm/src/lib.rs | 11 ++-- .../080_let_recursive_errors/Main.snap | 2 +- .../092_ambiguous_constraint/Main.snap | 4 +- .../142_derive_newtype_not_newtype/Main.snap | 2 +- .../Main.snap | 2 +- .../153_derive_contravariant_error/Main.snap | 4 +- .../155_derive_profunctor_error/Main.snap | 6 +- .../Main.snap | 2 +- .../Main.snap | 4 +- .../Main.snap | 2 +- tests-integration/src/generated/basic.rs | 59 ++++++++++++++++--- ...cking__constrained_invalid_constraint.snap | 4 +- .../checking__constrained_invalid_type.snap | 4 +- ...ecking__invalid_type_operator_nullary.snap | 2 +- ...ecking__invalid_type_operator_ternary.snap | 2 +- ...checking__invalid_type_operator_unary.snap | 2 +- .../snapshots/checking__partial_synonym.snap | 4 +- .../snapshots/checking__unification_fail.snap | 4 +- 36 files changed, 187 insertions(+), 129 deletions(-) diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_type_error.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_type_error.rs index 5e0e8e5b..6465fc2d 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_type_error.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_type_error.rs @@ -130,8 +130,7 @@ where return Some(MatchInstance::Stuck); }; - let message_id = state.checked.custom_messages.len() as u32; - state.checked.custom_messages.push(message); + let message_id = state.intern_error_message(message); state.insert_error(ErrorKind::CustomWarning { message_id }); Some(MatchInstance::Match { constraints: vec![], equalities: vec![] }) @@ -154,8 +153,7 @@ where return Some(MatchInstance::Stuck); }; - let message_id = state.checked.custom_messages.len() as u32; - state.checked.custom_messages.push(message); + let message_id = state.intern_error_message(message); state.insert_error(ErrorKind::CustomFailure { message_id }); Some(MatchInstance::Match { constraints: vec![], equalities: vec![] }) diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index 261963e5..c943ffe9 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -149,8 +149,8 @@ where }; let Some((data_file, data_id)) = extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; @@ -176,24 +176,21 @@ where return Ok(()); }; - let insert_error = - |state: &mut CheckState, context: &CheckContext, kind: fn(TypeId) -> ErrorKind| { - let global = transfer::globalize(state, context, newtype_type); - state.insert_error(kind(global)); - }; - let Some((newtype_file, newtype_id)) = extract_type_constructor(state, newtype_type) else { - insert_error(state, context, |type_id| ErrorKind::CannotDeriveForType { type_id }); + let type_message = state.render_local_type(context, newtype_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; if newtype_file != context.id { - insert_error(state, context, |type_id| ErrorKind::CannotDeriveForType { type_id }); + let type_message = state.render_local_type(context, newtype_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); } if !is_newtype(context, newtype_file, newtype_id)? { - insert_error(state, context, |type_id| ErrorKind::ExpectedNewtype { type_id }); + let type_message = state.render_local_type(context, newtype_type); + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); return Ok(()); } diff --git a/compiler-core/checking/src/algorithm/derive/contravariant.rs b/compiler-core/checking/src/algorithm/derive/contravariant.rs index 9e21cf17..4a7c7c46 100644 --- a/compiler-core/checking/src/algorithm/derive/contravariant.rs +++ b/compiler-core/checking/src/algorithm/derive/contravariant.rs @@ -6,7 +6,6 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::transfer; use crate::error::ErrorKind; /// Checks a derive instance for Contravariant. @@ -29,8 +28,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; @@ -65,8 +64,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/eq1.rs b/compiler-core/checking/src/algorithm/derive/eq1.rs index 9915e73a..e59c449e 100644 --- a/compiler-core/checking/src/algorithm/derive/eq1.rs +++ b/compiler-core/checking/src/algorithm/derive/eq1.rs @@ -17,7 +17,6 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::transfer; use crate::core::{Type, Variable, debruijn}; use crate::error::ErrorKind; @@ -82,8 +81,8 @@ where }; if derive::extract_type_constructor(state, derived_type).is_none() { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/foldable.rs b/compiler-core/checking/src/algorithm/derive/foldable.rs index 96b061b5..180434db 100644 --- a/compiler-core/checking/src/algorithm/derive/foldable.rs +++ b/compiler-core/checking/src/algorithm/derive/foldable.rs @@ -6,7 +6,6 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::transfer; use crate::error::ErrorKind; /// Checks a derive instance for Foldable. @@ -29,8 +28,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; @@ -65,8 +64,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/functor.rs b/compiler-core/checking/src/algorithm/derive/functor.rs index e7466672..0123af27 100644 --- a/compiler-core/checking/src/algorithm/derive/functor.rs +++ b/compiler-core/checking/src/algorithm/derive/functor.rs @@ -6,7 +6,6 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::transfer; use crate::error::ErrorKind; /// Checks a derive instance for Functor. @@ -29,8 +28,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; @@ -65,8 +64,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/generic.rs b/compiler-core/checking/src/algorithm/derive/generic.rs index d5ce8a76..d295a580 100644 --- a/compiler-core/checking/src/algorithm/derive/generic.rs +++ b/compiler-core/checking/src/algorithm/derive/generic.rs @@ -17,7 +17,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState, KnownGeneric}; -use crate::algorithm::{toolkit, transfer, unification}; +use crate::algorithm::{toolkit, unification}; use crate::core::{Type, TypeId}; use crate::error::ErrorKind; @@ -40,8 +40,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/newtype.rs b/compiler-core/checking/src/algorithm/derive/newtype.rs index 3c74bb3a..1a217ccd 100644 --- a/compiler-core/checking/src/algorithm/derive/newtype.rs +++ b/compiler-core/checking/src/algorithm/derive/newtype.rs @@ -5,7 +5,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{transfer, unification}; +use crate::algorithm::unification; use crate::error::ErrorKind; pub fn check_derive_newtype( @@ -28,14 +28,14 @@ where let Some((newtype_file, newtype_id)) = derive::extract_type_constructor(state, newtype_type) else { - let global_type = transfer::globalize(state, context, newtype_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, newtype_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; if !derive::is_newtype(context, newtype_file, newtype_id)? { - let global_type = transfer::globalize(state, context, newtype_type); - state.insert_error(ErrorKind::ExpectedNewtype { type_id: global_type }); + let type_message = state.render_local_type(context, newtype_type); + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); return Ok(()); } diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index b2ad20c1..37910019 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -9,10 +9,14 @@ use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{constraint, quantify, substitute, transfer}; +use crate::algorithm::{constraint, quantify, transfer}; use crate::core::{Instance, InstanceKind, Type, TypeId, debruijn}; use crate::error::ErrorKind; +mod substitute { + pub use crate::algorithm::substitute::SubstituteBindings; +} + /// Elaborated derive instance after kind inference. pub struct ElaboratedDerive { pub derive_id: DeriveId, @@ -91,7 +95,7 @@ where { let residual = state.solve_constraints(context)?; for constraint in residual { - let constraint = transfer::globalize(state, context, constraint); + let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); } Ok(()) diff --git a/compiler-core/checking/src/algorithm/derive/traversable.rs b/compiler-core/checking/src/algorithm/derive/traversable.rs index e32c4d2c..05616c81 100644 --- a/compiler-core/checking/src/algorithm/derive/traversable.rs +++ b/compiler-core/checking/src/algorithm/derive/traversable.rs @@ -6,7 +6,6 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; use crate::algorithm::derive::{self, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::transfer; use crate::error::ErrorKind; /// Checks a derive instance for Traversable. @@ -29,8 +28,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; @@ -65,8 +64,8 @@ where }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); return Ok(()); }; diff --git a/compiler-core/checking/src/algorithm/derive/variance.rs b/compiler-core/checking/src/algorithm/derive/variance.rs index f1a73357..ffa036a5 100644 --- a/compiler-core/checking/src/algorithm/derive/variance.rs +++ b/compiler-core/checking/src/algorithm/derive/variance.rs @@ -11,7 +11,7 @@ use crate::ExternalQueries; use crate::algorithm::derive::{self, tools}; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{substitute, toolkit, transfer}; +use crate::algorithm::{substitute, toolkit}; use crate::core::{RowType, Type, TypeId, Variable, debruijn}; use crate::error::ErrorKind; @@ -164,8 +164,8 @@ where DerivedParameter::new(*b, *b_config), ), _ => { - let global_type = transfer::globalize(state, context, derived_type); - state.insert_error(ErrorKind::CannotDeriveForType { type_id: global_type }); + let type_message = state.render_local_type(context, derived_type); + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); DerivedSkolems::Invalid } }; @@ -202,11 +202,11 @@ fn check_variance_field( if let Some(parameter) = skolems.get(level) && variance != parameter.expected { - let global = transfer::globalize(state, context, type_id); + let type_message = state.render_local_type(context, type_id); if variance == Variance::Covariant { - state.insert_error(ErrorKind::CovariantOccurrence { type_id: global }); + state.insert_error(ErrorKind::CovariantOccurrence { type_message }); } else { - state.insert_error(ErrorKind::ContravariantOccurrence { type_id: global }); + state.insert_error(ErrorKind::ContravariantOccurrence { type_message }); } } } @@ -225,14 +225,12 @@ fn check_variance_field( for parameter in skolems.iter() { if contains_skolem_level(state, argument, parameter.level) { if variance != parameter.expected { - let global = transfer::globalize(state, context, type_id); + let type_message = state.render_local_type(context, type_id); if variance == Variance::Covariant { - state.insert_error(ErrorKind::CovariantOccurrence { - type_id: global, - }); + state.insert_error(ErrorKind::CovariantOccurrence { type_message }); } else { state.insert_error(ErrorKind::ContravariantOccurrence { - type_id: global, + type_message, }); } } else if let Some(class) = parameter.class { diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 4009aff6..31090e3b 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -15,13 +15,14 @@ use lowering::{ }; use resolving::ResolvedModule; use rustc_hash::FxHashMap; +use smol_str::ToSmolStr; use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; use crate::algorithm::{constraint, transfer}; -use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn}; +use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; use crate::error::{CheckError, ErrorKind, ErrorStep}; -use crate::{CheckedModule, ExternalQueries}; +use crate::{CheckedModule, ExternalQueries, TypeErrorMessageId}; /// Manually-managed scope for type-level bindings. #[derive(Default)] @@ -978,6 +979,24 @@ impl CheckState { let error = CheckError { kind, step }; self.checked.errors.push(error); } + + /// Interns an error message in [`CheckedModule::error_messages`]. + pub fn intern_error_message(&mut self, message: impl ToSmolStr) -> TypeErrorMessageId { + self.checked.error_messages.intern(message.to_smolstr()) + } + + /// Renders a local type and interns it in [`CheckedModule::error_messages`]. + pub fn render_local_type( + &mut self, + context: &CheckContext, + t: TypeId, + ) -> TypeErrorMessageId + where + Q: ExternalQueries, + { + let t = pretty::print_local(self, context, t); + self.intern_error_message(t) + } } /// Functions for creating unification variables. diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 4a102c42..3cfb4b2a 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -190,7 +190,7 @@ where let residual = state.solve_constraints(context)?; for constraint in residual { - let constraint = transfer::globalize(state, context, constraint); + let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); } diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 007db08e..96711917 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -303,11 +303,11 @@ where state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { let _span = tracing::debug_span!("commit_value_group").entered(); for constraint in result.ambiguous { - let constraint = transfer::globalize(state, context, constraint); + let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::AmbiguousConstraint { constraint }); } for constraint in result.unsatisfied { - let constraint = transfer::globalize(state, context, constraint); + let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); } crate::debug_fields!(state, context, { quantified = result.quantified }); @@ -503,8 +503,8 @@ where if let Some(specialized_type) = specialized_type { let unified = unification::unify(state, context, member_type, specialized_type)?; if !unified { - let expected = transfer::globalize(state, context, specialized_type); - let actual = transfer::globalize(state, context, member_type); + let expected = state.render_local_type(context, specialized_type); + let actual = state.render_local_type(context, member_type); state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } } @@ -519,14 +519,14 @@ where let matches = unification::subtype(state, context, inferred_type, specialized_type)?; if !matches { - let expected = transfer::globalize(state, context, specialized_type); - let actual = transfer::globalize(state, context, inferred_type); + let expected = state.render_local_type(context, specialized_type); + let actual = state.render_local_type(context, inferred_type); state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } let residual = state.solve_constraints(context)?; for constraint in residual { - let constraint = transfer::globalize(state, context, constraint); + let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); } } diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index cdcfab9e..be93251b 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -608,7 +608,8 @@ where // Now that all items in the SCC are processed, the kind should be fully resolved if !is_binary_operator_type(state, kind) { - state.insert_error(ErrorKind::InvalidTypeOperator { id: kind }); + let kind_message = state.render_local_type(context, kind); + state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); } // Generalize and store the kind diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 8a94f56c..df312438 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -6,7 +6,7 @@ use itertools::{EitherOrBoth, Itertools}; use crate::ExternalQueries; use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{kind, substitute, transfer}; +use crate::algorithm::{kind, substitute}; use crate::core::{RowField, RowType, Type, TypeId, Variable, debruijn}; use crate::error::ErrorKind; @@ -172,8 +172,8 @@ where if !unifies { // at this point, it should be impossible to have // unsolved unification variables within t1 and t2 - let t1 = transfer::globalize(state, context, t1); - let t2 = transfer::globalize(state, context, t2); + let t1 = state.render_local_type(context, t1); + let t2 = state.render_local_type(context, t2); state.insert_error(ErrorKind::CannotUnify { t1, t2 }); } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 6e5178cd..55ce7586 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -2,7 +2,11 @@ use std::sync::Arc; -use crate::TypeId; +use interner::{Id, Interner}; +use smol_str::SmolStr; + +pub type TypeErrorMessageId = Id; +pub type TypeErrorMessageInterner = Interner; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorStep { @@ -23,24 +27,24 @@ pub enum ErrorStep { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorKind { AmbiguousConstraint { - constraint: TypeId, + constraint: TypeErrorMessageId, }, CannotDeriveClass { class_file: files::FileId, class_id: indexing::TypeItemId, }, CannotDeriveForType { - type_id: TypeId, + type_message: TypeErrorMessageId, }, ContravariantOccurrence { - type_id: TypeId, + type_message: TypeErrorMessageId, }, CovariantOccurrence { - type_id: TypeId, + type_message: TypeErrorMessageId, }, CannotUnify { - t1: TypeId, - t2: TypeId, + t1: TypeErrorMessageId, + t2: TypeErrorMessageId, }, DeriveInvalidArity { class_file: files::FileId, @@ -58,17 +62,17 @@ pub enum ErrorKind { actual: usize, }, InstanceMemberTypeMismatch { - expected: TypeId, - actual: TypeId, + expected: TypeErrorMessageId, + actual: TypeErrorMessageId, }, InvalidTypeOperator { - id: TypeId, + kind_message: TypeErrorMessageId, }, ExpectedNewtype { - type_id: TypeId, + type_message: TypeErrorMessageId, }, NoInstanceFound { - constraint: TypeId, + constraint: TypeErrorMessageId, }, PartialSynonymApplication { id: lowering::TypeId, @@ -98,10 +102,10 @@ pub enum ErrorKind { item_id: indexing::TypeItemId, }, CustomWarning { - message_id: u32, + message_id: TypeErrorMessageId, }, CustomFailure { - message_id: u32, + message_id: TypeErrorMessageId, }, } diff --git a/compiler-core/checking/src/lib.rs b/compiler-core/checking/src/lib.rs index 06ec0273..5304dd22 100644 --- a/compiler-core/checking/src/lib.rs +++ b/compiler-core/checking/src/lib.rs @@ -1,10 +1,12 @@ pub mod algorithm; -pub mod error; pub mod trace; pub mod core; pub use core::{Type, TypeId, TypeInterner}; +pub mod error; +pub use error::{TypeErrorMessageId, TypeErrorMessageInterner}; + use std::sync::Arc; use building_types::{QueryProxy, QueryResult}; @@ -49,7 +51,7 @@ pub struct CheckedModule { pub roles: FxHashMap>, pub errors: Vec, - pub custom_messages: Vec, + pub error_messages: TypeErrorMessageInterner, } impl CheckedModule { diff --git a/docs/src/wasm/src/lib.rs b/docs/src/wasm/src/lib.rs index a61260a7..d3576421 100644 --- a/docs/src/wasm/src/lib.rs +++ b/docs/src/wasm/src/lib.rs @@ -267,19 +267,16 @@ pub fn check(source: &str) -> JsValue { let mut errors = Vec::new(); for error in &checked.errors { + let message = |id| checked.error_messages[id].as_str(); let (kind, message) = match &error.kind { checking::error::ErrorKind::CannotUnify { t1, t2 } => { - let t1_pretty = pretty::print_global(engine, *t1); - let t2_pretty = pretty::print_global(engine, *t2); - ("CannotUnify".to_string(), format!("{t1_pretty} ~ {t2_pretty}")) + ("CannotUnify".to_string(), format!("{} ~ {}", message(*t1), message(*t2))) } checking::error::ErrorKind::NoInstanceFound { constraint } => { - let c_pretty = pretty::print_global(engine, *constraint); - ("NoInstanceFound".to_string(), c_pretty) + ("NoInstanceFound".to_string(), message(*constraint).to_string()) } checking::error::ErrorKind::AmbiguousConstraint { constraint } => { - let c_pretty = pretty::print_global(engine, *constraint); - ("AmbiguousConstraint".to_string(), c_pretty) + ("AmbiguousConstraint".to_string(), message(*constraint).to_string()) } _ => (format!("{:?}", error.kind), String::new()), }; diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index cb4f1672..2f06a749 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -14,6 +14,6 @@ Types Errors CannotUnify { Int, String } at [TermDeclaration(Idx::(1)), InferringExpression(AstId(20))] CannotUnify { String, Int } at [TermDeclaration(Idx::(1)), InferringExpression(AstId(20))] -CannotUnify { ??? -> ???, ??? } at [TermDeclaration(Idx::(2)), InferringExpression(AstId(61))] +CannotUnify { ?2[:0] -> ?3[:0], ?3[:0] } at [TermDeclaration(Idx::(2)), InferringExpression(AstId(61))] CannotUnify { Int, String } at [TermDeclaration(Idx::(3)), InferringExpression(AstId(82)), InferringExpression(AstId(102)), CheckingExpression(AstId(105))] CannotUnify { String, Int } at [TermDeclaration(Idx::(4)), InferringExpression(AstId(115)), InferringExpression(AstId(135)), CheckingExpression(AstId(138))] diff --git a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap index 3629ec60..509fcc01 100644 --- a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap +++ b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap @@ -16,5 +16,5 @@ class Read (&0 :: Type) class Show (&0 :: Type) Errors -AmbiguousConstraint { Show ??? } at [TermDeclaration(Idx::(2))] -AmbiguousConstraint { Read ??? } at [TermDeclaration(Idx::(2))] +AmbiguousConstraint { Show ?5[:0] } at [TermDeclaration(Idx::(2))] +AmbiguousConstraint { Read ?5[:0] } at [TermDeclaration(Idx::(2))] diff --git a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap index b57fee0c..caf9a22c 100644 --- a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap @@ -18,4 +18,4 @@ Roles Foo = [] Errors -ExpectedNewtype { type_id: Id(9) } at [TermDeclaration(Idx::(1))] +ExpectedNewtype { Foo } at [TermDeclaration(Idx::(1))] diff --git a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap index 05a05de4..e96c2a12 100644 --- a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap @@ -40,4 +40,4 @@ derive Functor (Reader (&0 :: Type) :: Type -> Type) derive Functor (Cont (&0 :: Type) :: Type -> Type) Errors -ContravariantOccurrence { type_id: Id(45) } at [TermDeclaration(Idx::(1))] +ContravariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] diff --git a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap index 2ee1b9db..8dfe1cc2 100644 --- a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap @@ -29,5 +29,5 @@ derive Contravariant (Identity :: Type -> Type) derive Contravariant (Producer :: Type -> Type) Errors -CovariantOccurrence { type_id: Id(34) } at [TermDeclaration(Idx::(1))] -CovariantOccurrence { type_id: Id(34) } at [TermDeclaration(Idx::(3))] +CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] +CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(3))] diff --git a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap index cdf626eb..0d3ec50a 100644 --- a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap @@ -32,6 +32,6 @@ derive Profunctor (WrongFirst :: Type -> Type -> Type) derive Profunctor (WrongSecond :: Type -> Type -> Type) Errors -CovariantOccurrence { type_id: Id(46) } at [TermDeclaration(Idx::(1))] -ContravariantOccurrence { type_id: Id(47) } at [TermDeclaration(Idx::(3))] -CovariantOccurrence { type_id: Id(46) } at [TermDeclaration(Idx::(3))] +CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] +ContravariantOccurrence { (~&1 :: Type) } at [TermDeclaration(Idx::(3))] +CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(3))] diff --git a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap index 25024b9a..b8de5c37 100644 --- a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap @@ -24,4 +24,4 @@ derive Bifunctor (Triple Int String :: Type -> Type) Errors CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(1)), CheckingKind(AstId(21))] -CannotDeriveForType { type_id: Id(46) } at [TermDeclaration(Idx::(1))] +CannotDeriveForType { Triple Int String } at [TermDeclaration(Idx::(1))] diff --git a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap index 7971d542..fca814be 100644 --- a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap @@ -30,6 +30,6 @@ derive Functor (Unit :: Type) Errors CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(1)), CheckingKind(AstId(19))] -CannotDeriveForType { type_id: Id(35) } at [TermDeclaration(Idx::(1))] +CannotDeriveForType { Pair Int String } at [TermDeclaration(Idx::(1))] CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(3)), CheckingKind(AstId(28))] -CannotDeriveForType { type_id: Id(17) } at [TermDeclaration(Idx::(3))] +CannotDeriveForType { Unit } at [TermDeclaration(Idx::(3))] diff --git a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap index 80d37ac7..fa19c3aa 100644 --- a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap @@ -18,4 +18,4 @@ Roles NotANewtype = [] Errors -ExpectedNewtype { type_id: Id(9) } at [TermDeclaration(Idx::(1))] +ExpectedNewtype { NotANewtype } at [TermDeclaration(Idx::(1))] diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 6dbfa91c..9c8e38d8 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -387,42 +387,85 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { } for error in &checked.errors { use checking::error::ErrorKind::*; - let pp = |t| pretty::print_global(engine, t); + let message = |id| &checked.error_messages[id]; let step = &error.step; match error.kind { CannotUnify { t1, t2 } => { - writeln!(snapshot, "CannotUnify {{ {}, {} }} at {step:?}", pp(t1), pp(t2)).unwrap(); + writeln!( + snapshot, + "CannotUnify {{ {}, {} }} at {step:?}", + message(t1), + message(t2) + ) + .unwrap(); } NoInstanceFound { constraint } => { - writeln!(snapshot, "NoInstanceFound {{ {} }} at {step:?}", pp(constraint)).unwrap(); + writeln!(snapshot, "NoInstanceFound {{ {} }} at {step:?}", message(constraint)) + .unwrap(); } AmbiguousConstraint { constraint } => { - writeln!(snapshot, "AmbiguousConstraint {{ {} }} at {step:?}", pp(constraint)) + writeln!(snapshot, "AmbiguousConstraint {{ {} }} at {step:?}", message(constraint)) .unwrap(); } InstanceMemberTypeMismatch { expected, actual } => { writeln!( snapshot, "InstanceMemberTypeMismatch {{ expected: {}, actual: {} }} at {step:?}", - pp(expected), - pp(actual) + message(expected), + message(actual) ) .unwrap(); } CustomWarning { message_id } => { - let message = &checked.custom_messages[message_id as usize]; + let message = message(message_id); writeln!(snapshot, "CustomWarning {{ .. }} at {step:?}").unwrap(); for line in message.lines() { writeln!(snapshot, " {line}").unwrap(); } } CustomFailure { message_id } => { - let message = &checked.custom_messages[message_id as usize]; + let message = message(message_id); writeln!(snapshot, "CustomFailure {{ .. }} at {step:?}").unwrap(); for line in message.lines() { writeln!(snapshot, " {line}").unwrap(); } } + CannotDeriveForType { type_message } => { + writeln!( + snapshot, + "CannotDeriveForType {{ {} }} at {step:?}", + message(type_message) + ) + .unwrap(); + } + ExpectedNewtype { type_message } => { + writeln!(snapshot, "ExpectedNewtype {{ {} }} at {step:?}", message(type_message)) + .unwrap(); + } + CovariantOccurrence { type_message } => { + writeln!( + snapshot, + "CovariantOccurrence {{ {} }} at {step:?}", + message(type_message) + ) + .unwrap(); + } + ContravariantOccurrence { type_message } => { + writeln!( + snapshot, + "ContravariantOccurrence {{ {} }} at {step:?}", + message(type_message) + ) + .unwrap(); + } + InvalidTypeOperator { kind_message } => { + writeln!( + snapshot, + "InvalidTypeOperator {{ {} }} at {step:?}", + message(kind_message) + ) + .unwrap(); + } _ => { writeln!(snapshot, "{:?} at {step:?}", error.kind).unwrap(); } diff --git a/tests-integration/tests/snapshots/checking__constrained_invalid_constraint.snap b/tests-integration/tests/snapshots/checking__constrained_invalid_constraint.snap index 61e4b949..c65154b6 100644 --- a/tests-integration/tests/snapshots/checking__constrained_invalid_constraint.snap +++ b/tests-integration/tests/snapshots/checking__constrained_invalid_constraint.snap @@ -5,8 +5,8 @@ expression: checked.errors [ CheckError { kind: CannotUnify { - t1: Id(1), - t2: Id(3), + t1: Id(1), + t2: Id(2), }, step: [ TypeDeclaration( diff --git a/tests-integration/tests/snapshots/checking__constrained_invalid_type.snap b/tests-integration/tests/snapshots/checking__constrained_invalid_type.snap index 0d722e5f..f3caf3c9 100644 --- a/tests-integration/tests/snapshots/checking__constrained_invalid_type.snap +++ b/tests-integration/tests/snapshots/checking__constrained_invalid_type.snap @@ -5,8 +5,8 @@ expression: checked.errors [ CheckError { kind: CannotUnify { - t1: Id(16), - t2: Id(1), + t1: Id(1), + t2: Id(2), }, step: [ TypeDeclaration( diff --git a/tests-integration/tests/snapshots/checking__invalid_type_operator_nullary.snap b/tests-integration/tests/snapshots/checking__invalid_type_operator_nullary.snap index b0baa088..b9cd089a 100644 --- a/tests-integration/tests/snapshots/checking__invalid_type_operator_nullary.snap +++ b/tests-integration/tests/snapshots/checking__invalid_type_operator_nullary.snap @@ -5,7 +5,7 @@ expression: checked.errors [ CheckError { kind: InvalidTypeOperator { - id: Id(1), + kind_message: Id(1), }, step: [], }, diff --git a/tests-integration/tests/snapshots/checking__invalid_type_operator_ternary.snap b/tests-integration/tests/snapshots/checking__invalid_type_operator_ternary.snap index 9ac2b28c..b9cd089a 100644 --- a/tests-integration/tests/snapshots/checking__invalid_type_operator_ternary.snap +++ b/tests-integration/tests/snapshots/checking__invalid_type_operator_ternary.snap @@ -5,7 +5,7 @@ expression: checked.errors [ CheckError { kind: InvalidTypeOperator { - id: Id(48), + kind_message: Id(1), }, step: [], }, diff --git a/tests-integration/tests/snapshots/checking__invalid_type_operator_unary.snap b/tests-integration/tests/snapshots/checking__invalid_type_operator_unary.snap index dc7c6071..b9cd089a 100644 --- a/tests-integration/tests/snapshots/checking__invalid_type_operator_unary.snap +++ b/tests-integration/tests/snapshots/checking__invalid_type_operator_unary.snap @@ -5,7 +5,7 @@ expression: checked.errors [ CheckError { kind: InvalidTypeOperator { - id: Id(36), + kind_message: Id(1), }, step: [], }, diff --git a/tests-integration/tests/snapshots/checking__partial_synonym.snap b/tests-integration/tests/snapshots/checking__partial_synonym.snap index f7c489b4..3c32f19c 100644 --- a/tests-integration/tests/snapshots/checking__partial_synonym.snap +++ b/tests-integration/tests/snapshots/checking__partial_synonym.snap @@ -18,8 +18,8 @@ expression: checked.errors }, CheckError { kind: CannotUnify { - t1: Id(1), - t2: Id(14), + t1: Id(1), + t2: Id(2), }, step: [ TypeDeclaration( diff --git a/tests-integration/tests/snapshots/checking__unification_fail.snap b/tests-integration/tests/snapshots/checking__unification_fail.snap index 41ed1902..96ab08d0 100644 --- a/tests-integration/tests/snapshots/checking__unification_fail.snap +++ b/tests-integration/tests/snapshots/checking__unification_fail.snap @@ -5,8 +5,8 @@ expression: checked.errors [ CheckError { kind: CannotUnify { - t1: Id(8), - t2: Id(1), + t1: Id(1), + t2: Id(2), }, step: [ TypeDeclaration( From e6ffcebb56deb8c37fbb69cb708c5c5df318e0fb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 21 Jan 2026 23:40:01 +0800 Subject: [PATCH 002/386] Initial scaffolding for diagnostics --- Cargo.lock | 17 ++ compiler-core/diagnostics/Cargo.toml | 18 ++ compiler-core/diagnostics/src/context.rs | 70 ++++++ compiler-core/diagnostics/src/convert.rs | 301 +++++++++++++++++++++++ compiler-core/diagnostics/src/lib.rs | 9 + compiler-core/diagnostics/src/model.rs | 94 +++++++ compiler-core/diagnostics/src/render.rs | 86 +++++++ 7 files changed, 595 insertions(+) create mode 100644 compiler-core/diagnostics/Cargo.toml create mode 100644 compiler-core/diagnostics/src/context.rs create mode 100644 compiler-core/diagnostics/src/convert.rs create mode 100644 compiler-core/diagnostics/src/lib.rs create mode 100644 compiler-core/diagnostics/src/model.rs create mode 100644 compiler-core/diagnostics/src/render.rs diff --git a/Cargo.lock b/Cargo.lock index d3543b09..15484da9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "diagnostics" +version = "0.1.0" +dependencies = [ + "checking", + "files", + "indexing", + "itertools 0.14.0", + "line-index", + "lowering", + "lsp-types", + "resolving", + "rowan", + "stabilizing", + "syntax", +] + [[package]] name = "digest" version = "0.10.7" diff --git a/compiler-core/diagnostics/Cargo.toml b/compiler-core/diagnostics/Cargo.toml new file mode 100644 index 00000000..271f5a94 --- /dev/null +++ b/compiler-core/diagnostics/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "diagnostics" +version = "0.1.0" +edition = "2024" + +[dependencies] +files = { version = "0.1.0", path = "../files" } +indexing = { version = "0.1.0", path = "../indexing" } +lowering = { version = "0.1.0", path = "../lowering" } +resolving = { version = "0.1.0", path = "../resolving" } +checking = { version = "0.1.0", path = "../checking" } +stabilizing = { version = "0.1.0", path = "../stabilizing" } +syntax = { version = "0.1.0", path = "../syntax" } + +rowan = "0.16.1" +lsp-types = "0.95.1" +line-index = "0.1.2" +itertools = "0.14.0" diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs new file mode 100644 index 00000000..fc39aca6 --- /dev/null +++ b/compiler-core/diagnostics/src/context.rs @@ -0,0 +1,70 @@ +use checking::{CheckedModule, error::ErrorStep}; +use indexing::IndexedModule; +use rowan::ast::{AstNode, AstPtr}; +use stabilizing::StabilizedModule; +use syntax::{SyntaxNode, SyntaxNodePtr}; + +use crate::Span; + +pub struct DiagnosticsContext<'a> { + pub content: &'a str, + pub root: &'a SyntaxNode, + pub stabilized: &'a StabilizedModule, + pub indexed: &'a IndexedModule, + pub checked: &'a CheckedModule, +} + +impl<'a> DiagnosticsContext<'a> { + pub fn new( + content: &'a str, + root: &'a SyntaxNode, + stabilized: &'a StabilizedModule, + indexed: &'a IndexedModule, + checked: &'a CheckedModule, + ) -> DiagnosticsContext<'a> { + DiagnosticsContext { content, root, stabilized, indexed, checked } + } + + pub fn span_from_syntax_ptr(&self, ptr: &SyntaxNodePtr) -> Option { + let range = ptr.to_node(self.root).text_range(); + Some(Span::new(range.start().into(), range.end().into())) + } + + pub fn span_from_ast_ptr>( + &self, + ptr: &AstPtr, + ) -> Option { + let ptr = ptr.syntax_node_ptr(); + self.span_from_syntax_ptr(&ptr) + } + + pub fn text_of(&self, span: Span) -> &'a str { + &self.content[span.start as usize..span.end as usize] + } + + pub fn span_from_error_step(&self, step: &ErrorStep) -> Option { + let ptr = match step { + ErrorStep::ConstructorArgument(id) => self.stabilized.syntax_ptr(*id)?, + ErrorStep::InferringKind(id) | ErrorStep::CheckingKind(id) => { + self.stabilized.syntax_ptr(*id)? + } + ErrorStep::InferringBinder(id) | ErrorStep::CheckingBinder(id) => { + self.stabilized.syntax_ptr(*id)? + } + ErrorStep::InferringExpression(id) | ErrorStep::CheckingExpression(id) => { + self.stabilized.syntax_ptr(*id)? + } + ErrorStep::TermDeclaration(id) => { + self.indexed.term_item_ptr(self.stabilized, *id).next()? + } + ErrorStep::TypeDeclaration(id) => { + self.indexed.type_item_ptr(self.stabilized, *id).next()? + } + }; + self.span_from_syntax_ptr(&ptr) + } + + pub fn primary_span_from_steps(&self, steps: &[ErrorStep]) -> Option { + steps.last().and_then(|step| self.span_from_error_step(step)) + } +} diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs new file mode 100644 index 00000000..c1961867 --- /dev/null +++ b/compiler-core/diagnostics/src/convert.rs @@ -0,0 +1,301 @@ +use checking::error::{CheckError, ErrorKind}; +use indexing::TypeItemKind; +use itertools::Itertools; +use lowering::LoweringError; +use resolving::ResolvingError; +use rowan::ast::AstNode; + +use crate::{Diagnostic, DiagnosticsContext, Severity}; + +pub trait ToDiagnostics { + fn to_diagnostics(&self, ctx: &DiagnosticsContext<'_>) -> Vec; +} + +impl ToDiagnostics for LoweringError { + fn to_diagnostics(&self, ctx: &DiagnosticsContext<'_>) -> Vec { + match self { + LoweringError::NotInScope(not_in_scope) => { + let (ptr, name) = match not_in_scope { + lowering::NotInScope::ExprConstructor { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::ExprVariable { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::ExprOperatorName { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::TypeConstructor { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::TypeVariable { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::TypeOperatorName { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::NegateFn { id } => { + (ctx.stabilized.syntax_ptr(*id), Some("negate")) + } + lowering::NotInScope::DoFn { kind, id } => ( + ctx.stabilized.syntax_ptr(*id), + match kind { + lowering::DoFn::Bind => Some("bind"), + lowering::DoFn::Discard => Some("discard"), + }, + ), + lowering::NotInScope::AdoFn { kind, id } => ( + ctx.stabilized.syntax_ptr(*id), + match kind { + lowering::AdoFn::Map => Some("map"), + lowering::AdoFn::Apply => Some("apply"), + lowering::AdoFn::Pure => Some("pure"), + }, + ), + lowering::NotInScope::TermOperator { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + lowering::NotInScope::TypeOperator { id } => { + (ctx.stabilized.syntax_ptr(*id), None) + } + }; + + let Some(ptr) = ptr else { return vec![] }; + let Some(span) = ctx.span_from_syntax_ptr(&ptr) else { return vec![] }; + + let message = if let Some(name) = name { + format!("'{name}' is not in scope") + } else { + let text = ctx.text_of(span).trim(); + format!("'{text}' is not in scope") + }; + + vec![Diagnostic::error("NotInScope", message, span, "lowering")] + } + + LoweringError::RecursiveSynonym(group) => convert_recursive_group( + ctx, + &group.group, + "RecursiveSynonym", + "Invalid type synonym cycle", + ), + + LoweringError::RecursiveKinds(group) => { + convert_recursive_group(ctx, &group.group, "RecursiveKinds", "Invalid kind cycle") + } + } + } +} + +fn convert_recursive_group( + ctx: &DiagnosticsContext<'_>, + group: &[indexing::TypeItemId], + code: &'static str, + message: &'static str, +) -> Vec { + let spans = group.iter().filter_map(|id| { + let ptr = match ctx.indexed.items[*id].kind { + TypeItemKind::Synonym { equation, .. } => ctx.stabilized.syntax_ptr(equation?)?, + TypeItemKind::Data { equation, .. } => ctx.stabilized.syntax_ptr(equation?)?, + TypeItemKind::Newtype { equation, .. } => ctx.stabilized.syntax_ptr(equation?)?, + _ => return None, + }; + ctx.span_from_syntax_ptr(&ptr) + }); + + let spans = spans.collect_vec(); + + let Some(&primary) = spans.first() else { return vec![] }; + + let mut diagnostic = Diagnostic::error(code, message, primary, "lowering"); + + for &span in &spans[1..] { + diagnostic = diagnostic.with_related(span, "Includes this type"); + } + + vec![diagnostic] +} + +impl ToDiagnostics for ResolvingError { + fn to_diagnostics(&self, ctx: &DiagnosticsContext<'_>) -> Vec { + match self { + ResolvingError::TermImportConflict { .. } + | ResolvingError::TypeImportConflict { .. } + | ResolvingError::TermExportConflict { .. } + | ResolvingError::TypeExportConflict { .. } + | ResolvingError::ExistingTerm { .. } + | ResolvingError::ExistingType { .. } => { + vec![] + } + + ResolvingError::InvalidImportStatement { id } => { + let Some(ptr) = ctx.stabilized.ast_ptr(*id) else { return vec![] }; + + let message = { + let cst = ptr.to_node(ctx.root); + let name = cst.module_name().map(|cst| { + let range = cst.syntax().text_range(); + ctx.content[range].trim() + }); + let name = name.unwrap_or(""); + format!("Cannot import module '{name}'") + }; + + let ptr = ptr.syntax_node_ptr(); + let Some(span) = ctx.span_from_syntax_ptr(&ptr) else { return vec![] }; + + vec![Diagnostic::error("InvalidImportStatement", message, span, "resolving")] + } + + ResolvingError::InvalidImportItem { id } => { + let Some(ptr) = ctx.stabilized.syntax_ptr(*id) else { return vec![] }; + let Some(span) = ctx.span_from_syntax_ptr(&ptr) else { return vec![] }; + + let text = ctx.text_of(span).trim(); + let message = format!("Cannot import item '{text}'"); + + vec![Diagnostic::error("InvalidImportItem", message, span, "resolving")] + } + } + } +} + +impl ToDiagnostics for CheckError { + fn to_diagnostics(&self, context: &DiagnosticsContext<'_>) -> Vec { + let Some(primary) = context.primary_span_from_steps(&self.step) else { + return vec![]; + }; + + let lookup_message = |id| context.checked.error_messages[id].as_str(); + + let (severity, code, message) = match &self.kind { + ErrorKind::AmbiguousConstraint { constraint } => { + let msg = lookup_message(*constraint); + (Severity::Error, "AmbiguousConstraint", format!("Ambiguous constraint: {msg}")) + } + ErrorKind::CannotDeriveClass { .. } => { + (Severity::Error, "CannotDeriveClass", "Cannot derive this class".to_string()) + } + ErrorKind::CannotDeriveForType { type_message } => { + let msg = lookup_message(*type_message); + (Severity::Error, "CannotDeriveForType", format!("Cannot derive for type: {msg}")) + } + ErrorKind::ContravariantOccurrence { type_message } => { + let msg = lookup_message(*type_message); + ( + Severity::Error, + "ContravariantOccurrence", + format!("Type variable occurs in contravariant position: {msg}"), + ) + } + ErrorKind::CovariantOccurrence { type_message } => { + let msg = lookup_message(*type_message); + ( + Severity::Error, + "CovariantOccurrence", + format!("Type variable occurs in covariant position: {msg}"), + ) + } + ErrorKind::CannotUnify { t1, t2 } => { + let t1 = lookup_message(*t1); + let t2 = lookup_message(*t2); + (Severity::Error, "CannotUnify", format!("Cannot unify '{t1}' with '{t2}'")) + } + ErrorKind::DeriveInvalidArity { expected, actual, .. } => ( + Severity::Error, + "DeriveInvalidArity", + format!("Invalid arity for derive: expected {expected}, got {actual}"), + ), + ErrorKind::DeriveMissingFunctor => ( + Severity::Error, + "DeriveMissingFunctor", + "Deriving Functor requires Data.Functor to be in scope".to_string(), + ), + ErrorKind::EmptyAdoBlock => { + (Severity::Error, "EmptyAdoBlock", "Empty ado block".to_string()) + } + ErrorKind::EmptyDoBlock => { + (Severity::Error, "EmptyDoBlock", "Empty do block".to_string()) + } + ErrorKind::InstanceHeadMismatch { expected, actual, .. } => ( + Severity::Error, + "InstanceHeadMismatch", + format!("Instance head mismatch: expected {expected} arguments, got {actual}"), + ), + ErrorKind::InstanceMemberTypeMismatch { expected, actual } => { + let expected = lookup_message(*expected); + let actual = lookup_message(*actual); + ( + Severity::Error, + "InstanceMemberTypeMismatch", + format!("Instance member type mismatch: expected '{expected}', got '{actual}'"), + ) + } + ErrorKind::InvalidTypeOperator { kind_message } => { + let msg = lookup_message(*kind_message); + ( + Severity::Error, + "InvalidTypeOperator", + format!("Invalid type operator kind: {msg}"), + ) + } + ErrorKind::ExpectedNewtype { type_message } => { + let msg = lookup_message(*type_message); + (Severity::Error, "ExpectedNewtype", format!("Expected a newtype, got: {msg}")) + } + ErrorKind::NoInstanceFound { constraint } => { + let msg = lookup_message(*constraint); + (Severity::Error, "NoInstanceFound", format!("No instance found for: {msg}")) + } + ErrorKind::PartialSynonymApplication { .. } => ( + Severity::Error, + "PartialSynonymApplication", + "Partial type synonym application".to_string(), + ), + ErrorKind::RecursiveSynonymExpansion { .. } => ( + Severity::Error, + "RecursiveSynonymExpansion", + "Recursive type synonym expansion".to_string(), + ), + ErrorKind::TooManyBinders { expected, actual, .. } => ( + Severity::Error, + "TooManyBinders", + format!("Too many binders: expected {expected}, got {actual}"), + ), + ErrorKind::TypeSignatureVariableMismatch { expected, actual, .. } => ( + Severity::Error, + "TypeSignatureVariableMismatch", + format!( + "Type signature variable mismatch: expected {expected} variables, got {actual}" + ), + ), + ErrorKind::InvalidRoleDeclaration { declared, inferred, .. } => ( + Severity::Error, + "InvalidRoleDeclaration", + format!("Invalid role declaration: declared {declared:?}, inferred {inferred:?}"), + ), + ErrorKind::CoercibleConstructorNotInScope { .. } => ( + Severity::Error, + "CoercibleConstructorNotInScope", + "Constructor not in scope for Coercible".to_string(), + ), + ErrorKind::CustomWarning { message_id } => { + let msg = lookup_message(*message_id); + (Severity::Warning, "CustomWarning", msg.to_string()) + } + ErrorKind::CustomFailure { message_id } => { + let msg = lookup_message(*message_id); + (Severity::Error, "CustomFailure", msg.to_string()) + } + }; + + vec![Diagnostic { + severity, + code: crate::DiagnosticCode::new(code), + message, + primary, + related: vec![], + source: "checking", + }] + } +} diff --git a/compiler-core/diagnostics/src/lib.rs b/compiler-core/diagnostics/src/lib.rs new file mode 100644 index 00000000..96beec57 --- /dev/null +++ b/compiler-core/diagnostics/src/lib.rs @@ -0,0 +1,9 @@ +mod context; +mod convert; +mod model; +mod render; + +pub use context::DiagnosticsContext; +pub use convert::ToDiagnostics; +pub use model::{Diagnostic, DiagnosticCode, RelatedSpan, Severity, Span}; +pub use render::{format_text, to_lsp_diagnostic}; diff --git a/compiler-core/diagnostics/src/model.rs b/compiler-core/diagnostics/src/model.rs new file mode 100644 index 00000000..1302b051 --- /dev/null +++ b/compiler-core/diagnostics/src/model.rs @@ -0,0 +1,94 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span { + pub start: u32, + pub end: u32, +} + +impl Span { + pub fn new(start: u32, end: u32) -> Span { + Span { start, end } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RelatedSpan { + pub span: Span, + pub message: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Severity { + Error, + Warning, +} + +use std::fmt; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DiagnosticCode(&'static str); + +impl DiagnosticCode { + pub fn new(code: &'static str) -> DiagnosticCode { + DiagnosticCode(code) + } +} + +impl fmt::Display for DiagnosticCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Diagnostic { + pub severity: Severity, + pub code: DiagnosticCode, + pub message: String, + pub primary: Span, + pub related: Vec, + pub source: &'static str, +} + +impl Diagnostic { + pub fn error( + code: &'static str, + message: impl Into, + primary: Span, + source: &'static str, + ) -> Diagnostic { + let message = message.into(); + let related = vec![]; + Diagnostic { + severity: Severity::Error, + code: DiagnosticCode::new(code), + message, + primary, + related, + source, + } + } + + pub fn warning( + code: &'static str, + message: impl Into, + primary: Span, + source: &'static str, + ) -> Diagnostic { + let message = message.into(); + let related = vec![]; + Diagnostic { + severity: Severity::Warning, + code: DiagnosticCode::new(code), + message, + primary, + related, + source, + } + } + + pub fn with_related(mut self, span: Span, message: impl Into) -> Diagnostic { + let message = message.into(); + self.related.push(RelatedSpan { span, message }); + self + } +} diff --git a/compiler-core/diagnostics/src/render.rs b/compiler-core/diagnostics/src/render.rs new file mode 100644 index 00000000..f0509856 --- /dev/null +++ b/compiler-core/diagnostics/src/render.rs @@ -0,0 +1,86 @@ +use itertools::Itertools; + +use crate::{Diagnostic, Severity}; + +pub fn format_text(diagnostics: &[Diagnostic]) -> String { + let mut output = String::new(); + + for diagnostic in diagnostics { + let severity = match diagnostic.severity { + Severity::Error => "error", + Severity::Warning => "warning", + }; + + output.push_str(&format!( + "{severity}[{}] at {}..{}: {}\n", + diagnostic.code, diagnostic.primary.start, diagnostic.primary.end, diagnostic.message + )); + + for related in &diagnostic.related { + output.push_str(&format!( + " note at {}..{}: {}\n", + related.span.start, related.span.end, related.message + )); + } + } + + output +} + +pub fn to_lsp_diagnostic( + diagnostic: &Diagnostic, + content: &str, + uri: &lsp_types::Url, +) -> Option { + use line_index::{LineCol, LineIndex}; + use lsp_types::{ + DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString, Position, Range, + }; + + let line_index = LineIndex::new(content); + + let to_position = |offset: u32| -> Option { + let LineCol { line, col } = line_index.line_col(offset.into()); + let line_range = line_index.line(line)?; + let line_content = &content[line_range]; + let until_col = &line_content[..col as usize]; + let character = until_col.chars().count() as u32; + Some(Position { line, character }) + }; + + let start = to_position(diagnostic.primary.start)?; + let end = to_position(diagnostic.primary.end)?; + let range = Range { start, end }; + + let severity = match diagnostic.severity { + Severity::Error => DiagnosticSeverity::ERROR, + Severity::Warning => DiagnosticSeverity::WARNING, + }; + + let related_information = diagnostic.related.iter().filter_map(|related| { + let start = to_position(related.span.start)?; + let end = to_position(related.span.end)?; + Some(DiagnosticRelatedInformation { + location: Location { uri: uri.clone(), range: Range { start, end } }, + message: related.message.clone(), + }) + }); + + let related_information = related_information.collect_vec(); + + Some(lsp_types::Diagnostic { + range, + severity: Some(severity), + code: Some(NumberOrString::String(diagnostic.code.to_string())), + code_description: None, + source: Some(format!("analyzer/{}", diagnostic.source)), + message: diagnostic.message.clone(), + related_information: if related_information.is_empty() { + None + } else { + Some(related_information) + }, + tags: None, + data: None, + }) +} From 347aa0da1f5edfd51408d8f502588fd3f6d3db74 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 21 Jan 2026 23:52:16 +0800 Subject: [PATCH 003/386] Render diagnostics in type checking tests --- Cargo.lock | 1 + .../checking/src/algorithm/type_item.rs | 7 +- compiler-core/checking/src/error.rs | 3 +- tests-integration/Cargo.toml | 1 + .../032_recursive_synonym_expansion/Main.snap | 20 ++-- .../078_inspect_arity_invalid/Main.snap | 8 +- .../080_let_recursive_errors/Main.snap | 12 +- .../checking/084_instance_eq/Main.snap | 4 +- .../checking/089_no_instance_found/Main.snap | 4 +- .../092_ambiguous_constraint/Main.snap | 6 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../111_int_add_invalid_no_instance/Main.snap | 4 +- .../112_int_mul_invalid_no_instance/Main.snap | 4 +- .../Main.snap | 5 +- .../Main.snap | 4 +- .../checking/115_empty_do_block/Main.snap | 6 +- .../checking/116_empty_ado_block/Main.snap | 6 +- .../checking/117_do_ado_constrained/Main.snap | 4 +- .../Main.snap | 8 +- .../123_incomplete_instance_head/Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 6 +- .../checking/127_derive_eq_simple/Main.snap | 4 +- .../131_derive_eq_missing_instance/Main.snap | 4 +- .../132_derive_eq_1_higher_kinded/Main.snap | 4 +- .../checking/133_derive_eq_partial/Main.snap | 4 +- .../checking/134_derive_ord_simple/Main.snap | 4 +- .../135_derive_ord_1_higher_kinded/Main.snap | 6 +- .../136_derive_nested_higher_kinded/Main.snap | 10 +- .../142_derive_newtype_not_newtype/Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 6 +- .../Main.snap | 6 +- .../153_derive_contravariant_error/Main.snap | 6 +- .../155_derive_profunctor_error/Main.snap | 8 +- .../Main.snap | 6 +- .../Main.snap | 10 +- .../Main.snap | 4 +- .../Main.snap | 6 +- .../Main.snap | 6 +- .../checking/167_derive_eq_1/Main.snap | 6 +- .../checking/168_derive_ord_1/Main.snap | 8 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../checking/190_coercible_nominal/Main.snap | 4 +- .../191_coercible_newtype_hidden/Main.snap | 10 +- .../Main.snap | 6 +- .../Main.snap | 4 +- .../202_int_compare_invalid/Main.snap | 17 ++- .../checking/205_builtin_warn/Main.snap | 28 ++--- .../checking/206_builtin_fail/Main.snap | 10 +- tests-integration/src/generated/basic.rs | 113 +++++------------- 57 files changed, 195 insertions(+), 266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15484da9..2dd47588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1874,6 +1874,7 @@ dependencies = [ "async-lsp", "checking", "convert_case", + "diagnostics", "files", "glob", "indexing", diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index be93251b..b04d896c 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -1022,11 +1022,8 @@ fn check_roles( if is_foreign || declared >= inferred { *validated = declared; } else { - state.insert_error(ErrorKind::InvalidRoleDeclaration { - type_id, - parameter_index: index, - declared, - inferred, + state.with_error_step(ErrorStep::TypeDeclaration(type_id), |state| { + state.insert_error(ErrorKind::InvalidRoleDeclaration { index, declared, inferred }); }); } } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 55ce7586..f55fd050 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -92,8 +92,7 @@ pub enum ErrorKind { actual: u32, }, InvalidRoleDeclaration { - type_id: indexing::TypeItemId, - parameter_index: usize, + index: usize, declared: crate::core::Role, inferred: crate::core::Role, }, diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index d1fe8d5c..b2c28035 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -8,6 +8,7 @@ build = "build.rs" analyzer = { version = "0.1.0", path = "../compiler-lsp/analyzer" } async-lsp = "0.2.2" checking = { version = "0.1.0", path = "../compiler-core/checking" } +diagnostics = { version = "0.1.0", path = "../compiler-core/diagnostics" } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } files = { version = "0.1.0", path = "../compiler-core/files" } diff --git a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap index 91ec8369..83a5a8a8 100644 --- a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap @@ -36,13 +36,13 @@ Valid = Int Type = :0 -Errors -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(0))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(0))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(0))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(1) } at [TermDeclaration(Idx::(1))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(1) } at [TermDeclaration(Idx::(1))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(1) } at [TermDeclaration(Idx::(1))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(2) } at [TermDeclaration(Idx::(2))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(2) } at [TermDeclaration(Idx::(2))] -RecursiveSynonymExpansion { file_id: Idx::(30), item_id: Idx::(2) } at [TermDeclaration(Idx::(2))] +Diagnostics +error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion diff --git a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap index 22969a05..20164259 100644 --- a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap +++ b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap @@ -35,7 +35,7 @@ Const = forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type Type = :0 -Errors -TooManyBinders { signature: AstId(13), expected: 2, actual: 3 } at [TermDeclaration(Idx::(0))] -TooManyBinders { signature: AstId(39), expected: 2, actual: 3 } at [TermDeclaration(Idx::(1))] -TooManyBinders { signature: AstId(62), expected: 2, actual: 3 } at [TermDeclaration(Idx::(2))] +Diagnostics +error[TooManyBinders] at 92..126: Too many binders: expected 2, got 3 +error[TooManyBinders] at 206..247: Too many binders: expected 2, got 3 +error[TooManyBinders] at 308..329: Too many binders: expected 2, got 3 diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index 2f06a749..0b744ecc 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -11,9 +11,9 @@ threeWayConflict :: Int -> Int Types -Errors -CannotUnify { Int, String } at [TermDeclaration(Idx::(1)), InferringExpression(AstId(20))] -CannotUnify { String, Int } at [TermDeclaration(Idx::(1)), InferringExpression(AstId(20))] -CannotUnify { ?2[:0] -> ?3[:0], ?3[:0] } at [TermDeclaration(Idx::(2)), InferringExpression(AstId(61))] -CannotUnify { Int, String } at [TermDeclaration(Idx::(3)), InferringExpression(AstId(82)), InferringExpression(AstId(102)), CheckingExpression(AstId(105))] -CannotUnify { String, Int } at [TermDeclaration(Idx::(4)), InferringExpression(AstId(115)), InferringExpression(AstId(135)), CheckingExpression(AstId(138))] +Diagnostics +error[CannotUnify] at 160..248: Cannot unify 'Int' with 'String' +error[CannotUnify] at 160..248: Cannot unify 'String' with 'Int' +error[CannotUnify] at 353..376: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' +error[CannotUnify] at 520..522: Cannot unify 'Int' with 'String' +error[CannotUnify] at 659..666: Cannot unify 'String' with 'Int' diff --git a/tests-integration/fixtures/checking/084_instance_eq/Main.snap b/tests-integration/fixtures/checking/084_instance_eq/Main.snap index 083c9ca9..459e81eb 100644 --- a/tests-integration/fixtures/checking/084_instance_eq/Main.snap +++ b/tests-integration/fixtures/checking/084_instance_eq/Main.snap @@ -16,5 +16,5 @@ Instances instance Eq (Int :: Type) chain: 0 -Errors -NoInstanceFound { Eq String } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 101..138: No instance found for: Eq String diff --git a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap index 9595f74e..ba203e7d 100644 --- a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap +++ b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap @@ -23,5 +23,5 @@ Foo = [] Classes class Eq (&0 :: Type) -Errors -NoInstanceFound { Eq Foo } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 77..149: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap index 509fcc01..cae94bab 100644 --- a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap +++ b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap @@ -15,6 +15,6 @@ Classes class Read (&0 :: Type) class Show (&0 :: Type) -Errors -AmbiguousConstraint { Show ?5[:0] } at [TermDeclaration(Idx::(2))] -AmbiguousConstraint { Read ?5[:0] } at [TermDeclaration(Idx::(2))] +Diagnostics +error[AmbiguousConstraint] at 101..257: Ambiguous constraint: Show ?5[:0] +error[AmbiguousConstraint] at 101..257: Ambiguous constraint: Read ?5[:0] diff --git a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap index a320ac8e..5922231a 100644 --- a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap +++ b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap @@ -23,5 +23,5 @@ Foo = [] Classes class Eq (&0 :: Type) -Errors -NoInstanceFound { Eq Foo } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 77..227: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap index d4097d77..a4da9e47 100644 --- a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap @@ -19,5 +19,5 @@ Proxy Roles Proxy = [Phantom] -Errors -NoInstanceFound { Lacks @Type "b" ( a :: Int, b :: String ) } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 196..222: No instance found for: Lacks @Type "b" ( a :: Int, b :: String ) diff --git a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap index 8889cc1b..766e24a1 100644 --- a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap @@ -19,5 +19,5 @@ Proxy Roles Proxy = [Phantom] -Errors -NoInstanceFound { Add 2 3 10 } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 152..178: No instance found for: Add 2 3 10 diff --git a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap index ac88bd4d..f1506305 100644 --- a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap @@ -19,5 +19,5 @@ Proxy Roles Proxy = [Phantom] -Errors -NoInstanceFound { Mul 2 3 10 } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 152..178: No instance found for: Mul 2 3 10 diff --git a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap index 137d5fb7..99227aa5 100644 --- a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap @@ -19,5 +19,6 @@ Proxy Roles Proxy = [Phantom] -Errors -NoInstanceFound { Compare 5 1 LT } at [TermDeclaration(Idx::(2))] +Diagnostics +error[InvalidImportItem] at 64..68: Cannot import item 'kind' +error[NoInstanceFound] at 197..223: No instance found for: Compare 5 1 LT diff --git a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap index da18ef47..65c22ffb 100644 --- a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap @@ -19,5 +19,5 @@ Proxy Roles Proxy = [Phantom] -Errors -NoInstanceFound { ToString 42 "999" } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 162..188: No instance found for: ToString 42 "999" diff --git a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap index 7eb99fbf..7fa0496d 100644 --- a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap +++ b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap @@ -18,6 +18,6 @@ Effect :: Type -> Type Roles Effect = [Nominal] -Errors -EmptyDoBlock at [TermDeclaration(Idx::(3)), InferringExpression(AstId(56))] -CannotUnify { Type, ??? } at [TermDeclaration(Idx::(3))] +Diagnostics +error[EmptyDoBlock] at 270..273: Empty do block +error[CannotUnify] at 262..273: Cannot unify 'Type' with '???' diff --git a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap index 4b2279dc..dc98a6f4 100644 --- a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap +++ b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap @@ -19,6 +19,6 @@ Effect :: Type -> Type Roles Effect = [Nominal] -Errors -EmptyAdoBlock at [TermDeclaration(Idx::(3)), InferringExpression(AstId(54))] -CannotUnify { Type, ??? } at [TermDeclaration(Idx::(3))] +Diagnostics +error[EmptyAdoBlock] at 261..265: Empty ado block +error[CannotUnify] at 252..265: Cannot unify 'Type' with '???' diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 5c8227e0..246c6744 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -72,5 +72,5 @@ class Applicative (&0 :: Type -> Type) <= Discard (&0 :: Type -> Type) class Applicative (&0 :: Type -> Type) <= Bind (&0 :: Type -> Type) class Bind (&0 :: Type -> Type) <= Monad (&0 :: Type -> Type) -Errors -NoInstanceFound { Discard (&0 :: Type -> Type) } at [TermDeclaration(Idx::(10))] +Diagnostics +error[NoInstanceFound] at 833..878: No instance found for: Discard (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap index 52e71db9..56a3fa83 100644 --- a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap +++ b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap @@ -15,7 +15,7 @@ Instances instance Show (Int :: Type) chain: 0 -Errors -CannotUnify { Int, String } at [TermDeclaration(Idx::(1))] -CannotUnify { Int -> Int, Int -> String } at [TermDeclaration(Idx::(1))] -InstanceMemberTypeMismatch { expected: Int -> String, actual: Int -> Int } at [TermDeclaration(Idx::(1))] +Diagnostics +error[CannotUnify] at 59..118: Cannot unify 'Int' with 'String' +error[CannotUnify] at 59..118: Cannot unify 'Int -> Int' with 'Int -> String' +error[InstanceMemberTypeMismatch] at 59..118: Instance member type mismatch: expected 'Int -> String', got 'Int -> Int' diff --git a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap index e4fd1f86..c49393bc 100644 --- a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap +++ b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap @@ -20,5 +20,5 @@ instance Pair (Int :: Type) (String :: Type) instance Pair (Int :: Type) chain: 0 -Errors -InstanceHeadMismatch { class_file: Idx::(30), class_item: Idx::(0), expected: 2, actual: 1 } at [TermDeclaration(Idx::(2))] +Diagnostics +error[InstanceHeadMismatch] at 138..191: Instance head mismatch: expected 2 arguments, got 1 diff --git a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap index 1db76903..fc4e2efb 100644 --- a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap +++ b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap @@ -34,5 +34,5 @@ Instances instance Functor (Box :: Type -> Type) chain: 0 -Errors -NoInstanceFound { Show (~&1 :: Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 146..240: No instance found for: Show (~&1 :: Type) diff --git a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap index 1cfd3b3c..7a8565c3 100644 --- a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap +++ b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap @@ -15,6 +15,6 @@ Instances instance Show (Boolean :: Type) chain: 0 -Errors -CannotUnify { forall (a :: Type). (a :: Type) -> String, Boolean -> String } at [TermDeclaration(Idx::(1))] -InstanceMemberTypeMismatch { expected: Boolean -> String, actual: forall (a :: Type). (a :: Type) -> String } at [TermDeclaration(Idx::(1))] +Diagnostics +error[CannotUnify] at 59..138: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' +error[InstanceMemberTypeMismatch] at 59..138: Instance member type mismatch: expected 'Boolean -> String', got 'forall (a :: Type). (a :: Type) -> String' diff --git a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap index e22867b9..bb89c061 100644 --- a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap +++ b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap @@ -35,5 +35,5 @@ Derived derive forall (&0 :: Type). Eq (Proxy @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type) derive Eq (ContainsNoEq :: Type) -Errors -NoInstanceFound { Eq NoEq } at [TermDeclaration(Idx::(4))] +Diagnostics +error[NoInstanceFound] at 157..190: No instance found for: Eq NoEq diff --git a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap index 72d81bda..60e279d4 100644 --- a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap @@ -27,5 +27,5 @@ Box = [] Derived derive Eq (Box :: Type) -Errors -NoInstanceFound { Eq NoEq } at [TermDeclaration(Idx::(2))] +Diagnostics +error[NoInstanceFound] at 87..111: No instance found for: Eq NoEq diff --git a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap index 60acc659..c7742fcd 100644 --- a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap @@ -34,5 +34,5 @@ Derived derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) derive Eq (&1 :: Type) => Eq (WrapNoEq1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -Errors -NoInstanceFound { Eq1 (&0 :: Type -> Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 214..258: No instance found for: Eq1 (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap index f017cb82..583d0c4f 100644 --- a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap +++ b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap @@ -22,5 +22,5 @@ Derived derive Eq (&0 :: Type) => Eq (Either Int (&0 :: Type) :: Type) derive Eq (Either Int (&0 :: Type) :: Type) -Errors -NoInstanceFound { Eq (&0 :: Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 123..158: No instance found for: Eq (&0 :: Type) diff --git a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap index ffce0587..c456ea9e 100644 --- a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap +++ b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap @@ -47,5 +47,5 @@ derive Eq (NoOrd :: Type) derive Eq (ContainsNoOrd :: Type) derive Ord (ContainsNoOrd :: Type) -Errors -NoInstanceFound { Ord NoOrd } at [TermDeclaration(Idx::(10))] +Diagnostics +error[NoInstanceFound] at 350..384: No instance found for: Ord NoOrd diff --git a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap index 2a435bb5..4d2d5d3f 100644 --- a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap @@ -36,6 +36,6 @@ derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: T derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) derive Ord (&1 :: Type) => Ord (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -Errors -NoInstanceFound { Ord1 (&0 :: Type -> Type) } at [TermDeclaration(Idx::(5))] -NoInstanceFound { Eq1 (&0 :: Type -> Type) } at [TermDeclaration(Idx::(5))] +Diagnostics +error[NoInstanceFound] at 319..365: No instance found for: Ord1 (&0 :: Type -> Type) +error[NoInstanceFound] at 319..365: No instance found for: Eq1 (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap index 2487ddad..b1876c56 100644 --- a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap @@ -55,8 +55,8 @@ derive Ord (&0 :: Type) => Ord (Maybe (&0 :: Type) :: Type) derive Eq1 (&0 :: Type -> Type) => Eq (U (&0 :: Type -> Type) :: Type) derive Ord1 (&0 :: Type -> Type) => Ord (U (&0 :: Type -> Type) :: Type) -Errors -NoInstanceFound { Eq ((&1 :: Type -> Type) Int) } at [TermDeclaration(Idx::(1))] -NoInstanceFound { Ord ((&1 :: Type -> Type) Int) } at [TermDeclaration(Idx::(2))] -NoInstanceFound { Eq ((&1 :: Int -> Type) 42) } at [TermDeclaration(Idx::(11))] -NoInstanceFound { Ord ((&1 :: Int -> Type) 42) } at [TermDeclaration(Idx::(12))] +Diagnostics +error[NoInstanceFound] at 123..162: No instance found for: Eq ((&1 :: Type -> Type) Int) +error[NoInstanceFound] at 162..202: No instance found for: Ord ((&1 :: Type -> Type) Int) +error[NoInstanceFound] at 444..483: No instance found for: Eq ((&1 :: Int -> Type) 42) +error[NoInstanceFound] at 483..523: No instance found for: Ord ((&1 :: Int -> Type) 42) diff --git a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap index caf9a22c..672fe1ed 100644 --- a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap @@ -17,5 +17,5 @@ Foo Roles Foo = [] -Errors -ExpectedNewtype { Foo } at [TermDeclaration(Idx::(1))] +Diagnostics +error[ExpectedNewtype] at 68..102: Expected a newtype, got: Foo diff --git a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap index 480797cd..df4ebac2 100644 --- a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap @@ -20,5 +20,5 @@ Identity = [Representational] Derived derive Show (Identity String :: Type) -Errors -NoInstanceFound { Show String } at [TermDeclaration(Idx::(1))] +Diagnostics +error[NoInstanceFound] at 81..129: No instance found for: Show String diff --git a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap index 0b2c6fb5..6d4ac5b3 100644 --- a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap +++ b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap @@ -20,5 +20,5 @@ Identity = [Representational] Derived derive Show (Identity (&0 :: Type) :: Type) -Errors -NoInstanceFound { Show (&0 :: Type) } at [TermDeclaration(Idx::(1))] +Diagnostics +error[NoInstanceFound] at 81..124: No instance found for: Show (&0 :: Type) diff --git a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap index 4c3642bd..dbf55688 100644 --- a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap @@ -34,5 +34,5 @@ Derived derive Functor (&0 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) derive Functor (WrapNoFunctor @Type (&0 :: Type -> Type) :: Type -> Type) -Errors -NoInstanceFound { Functor (&0 :: Type -> Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 174..216: No instance found for: Functor (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap index e96c2a12..15e8b55a 100644 --- a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap @@ -39,5 +39,5 @@ derive Functor (Predicate :: Type -> Type) derive Functor (Reader (&0 :: Type) :: Type -> Type) derive Functor (Cont (&0 :: Type) :: Type -> Type) -Errors -ContravariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] +Diagnostics +error[ContravariantOccurrence] at 99..133: Type variable occurs in contravariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap index 39402b81..ac516733 100644 --- a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap @@ -50,6 +50,6 @@ Derived derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) derive Bifunctor (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -Errors -NoInstanceFound { Functor (&0 :: Type -> Type) } at [TermDeclaration(Idx::(3))] -NoInstanceFound { Functor (&1 :: Type -> Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 277..330: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound] at 277..330: No instance found for: Functor (&1 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap index 80506f29..1284d420 100644 --- a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap +++ b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap @@ -31,6 +31,6 @@ WrapBoth = [Representational, Representational, Nominal, Nominal] Derived derive Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -Errors -DeriveMissingFunctor at [TermDeclaration(Idx::(1))] -DeriveMissingFunctor at [TermDeclaration(Idx::(1))] +Diagnostics +error[DeriveMissingFunctor] at 104..145: Deriving Functor requires Data.Functor to be in scope +error[DeriveMissingFunctor] at 104..145: Deriving Functor requires Data.Functor to be in scope diff --git a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap index 8dfe1cc2..2465de7b 100644 --- a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap @@ -28,6 +28,6 @@ Derived derive Contravariant (Identity :: Type -> Type) derive Contravariant (Producer :: Type -> Type) -Errors -CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] -CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[CovariantOccurrence] at 168..207: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence] at 308..347: Type variable occurs in covariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap index 0d3ec50a..2649fe85 100644 --- a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap @@ -31,7 +31,7 @@ Derived derive Profunctor (WrongFirst :: Type -> Type -> Type) derive Profunctor (WrongSecond :: Type -> Type -> Type) -Errors -CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(1))] -ContravariantOccurrence { (~&1 :: Type) } at [TermDeclaration(Idx::(3))] -CovariantOccurrence { (~&0 :: Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[CovariantOccurrence] at 150..188: Type variable occurs in covariant position: (~&0 :: Type) +error[ContravariantOccurrence] at 290..329: Type variable occurs in contravariant position: (~&1 :: Type) +error[CovariantOccurrence] at 290..329: Type variable occurs in covariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap index b8de5c37..294eb78f 100644 --- a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap @@ -22,6 +22,6 @@ Triple = [Representational, Representational, Representational] Derived derive Bifunctor (Triple Int String :: Type -> Type) -Errors -CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(1)), CheckingKind(AstId(21))] -CannotDeriveForType { Triple Int String } at [TermDeclaration(Idx::(1))] +Diagnostics +error[CannotUnify] at 118..138: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 92..138: Cannot derive for type: Triple Int String diff --git a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap index fca814be..b14639e7 100644 --- a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap @@ -28,8 +28,8 @@ Derived derive Functor (Pair Int String :: Type) derive Functor (Unit :: Type) -Errors -CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(1)), CheckingKind(AstId(19))] -CannotDeriveForType { Pair Int String } at [TermDeclaration(Idx::(1))] -CannotUnify { Type, Type -> Type } at [TermDeclaration(Idx::(3)), CheckingKind(AstId(28))] -CannotDeriveForType { Unit } at [TermDeclaration(Idx::(3))] +Diagnostics +error[CannotUnify] at 104..122: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 80..122: Cannot derive for type: Pair Int String +error[CannotUnify] at 164..169: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 140..169: Cannot derive for type: Unit diff --git a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap index f53133c1..2210f2c3 100644 --- a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap @@ -34,5 +34,5 @@ Derived derive Foldable (&0 :: Type -> Type) => Foldable (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) derive Foldable (WrapNoFoldable @Type (&0 :: Type -> Type) :: Type -> Type) -Errors -NoInstanceFound { Foldable (&0 :: Type -> Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 180..224: No instance found for: Foldable (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap index 5e87e9c1..e1f05b43 100644 --- a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap @@ -50,6 +50,6 @@ Derived derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Bifoldable (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) derive Bifoldable (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -Errors -NoInstanceFound { Foldable (&0 :: Type -> Type) } at [TermDeclaration(Idx::(3))] -NoInstanceFound { Foldable (&1 :: Type -> Type) } at [TermDeclaration(Idx::(3))] +Diagnostics +error[NoInstanceFound] at 284..338: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound] at 284..338: No instance found for: Foldable (&1 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap index 08521005..d1962ab7 100644 --- a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap +++ b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap @@ -29,6 +29,6 @@ Compose = [Representational, Representational, Nominal] Derived derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Traversable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -Errors -NoInstanceFound { Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) } at [TermDeclaration(Idx::(1))] -NoInstanceFound { Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) } at [TermDeclaration(Idx::(1))] +Diagnostics +error[NoInstanceFound] at 174..250: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound] at 174..250: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index 889e3cf9..58cae96f 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -96,6 +96,6 @@ derive Eq (&0 :: Type) => Eq (Rec (&0 :: Type) :: Type) derive Eq1 (Rec :: Type -> Type) derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -Errors -NoInstanceFound { Eq ((&1 :: Type -> Type) (~&2 :: Type)) } at [TermDeclaration(Idx::(17))] -NoInstanceFound { Eq (NoEq (~&0 :: Type)) } at [TermDeclaration(Idx::(23))] +Diagnostics +error[NoInstanceFound] at 635..753: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 916..942: No instance found for: Eq (NoEq (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index 60e8c712..3f00c8e1 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -67,7 +67,7 @@ derive Eq1 (&0 :: Type -> Type) => Eq1 (Wrap @Type (&0 :: Type -> Type) :: Type derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) derive Ord1 (&0 :: Type -> Type) => Ord1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) -Errors -NoInstanceFound { Eq ((&1 :: Type -> Type) (~&2 :: Type)) } at [TermDeclaration(Idx::(13))] -NoInstanceFound { Ord ((&1 :: Type -> Type) (~&2 :: Type)) } at [TermDeclaration(Idx::(14))] -NoInstanceFound { Ord (NoOrd (~&0 :: Type)) } at [TermDeclaration(Idx::(18))] +Diagnostics +error[NoInstanceFound] at 591..707: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 707..752: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 877..904: No instance found for: Ord (NoOrd (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap index fa19c3aa..f5197258 100644 --- a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap @@ -17,5 +17,5 @@ NotANewtype Roles NotANewtype = [] -Errors -ExpectedNewtype { NotANewtype } at [TermDeclaration(Idx::(1))] +Diagnostics +error[ExpectedNewtype] at 90..129: Expected a newtype, got: NotANewtype diff --git a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap index ddbd408c..7c82a000 100644 --- a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap +++ b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap @@ -20,5 +20,5 @@ F Roles F = [Representational, Nominal] -Errors -InvalidRoleDeclaration { type_id: Idx::(0), parameter_index: 1, declared: Phantom, inferred: Nominal } at [] +Diagnostics +error[InvalidRoleDeclaration] at 17..39: Invalid role declaration: declared Phantom, inferred Nominal diff --git a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap index b269ccaf..f321e1fe 100644 --- a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap +++ b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap @@ -27,5 +27,5 @@ Roles Maybe = [Representational] Either = [Representational, Representational] -Errors -NoInstanceFound { Coercible @Type (Maybe Int) (Either Int String) } at [TermDeclaration(Idx::(4))] +Diagnostics +error[NoInstanceFound] at 114..165: No instance found for: Coercible @Type (Maybe Int) (Either Int String) diff --git a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap index 3858b498..47b992da 100644 --- a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap +++ b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap @@ -12,5 +12,5 @@ Nominal :: Type -> Type Roles Nominal = [Nominal] -Errors -NoInstanceFound { Coercible @Type (Nominal Int) (Nominal String) } at [TermDeclaration(Idx::(1))] +Diagnostics +error[NoInstanceFound] at 194..251: No instance found for: Coercible @Type (Nominal Int) (Nominal String) diff --git a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap index 4fa9f60e..5116f9f8 100644 --- a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap +++ b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap @@ -8,8 +8,8 @@ coerceQualified :: Int -> HiddenAge Types -Errors -CoercibleConstructorNotInScope { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(0))] -NoInstanceFound { Coercible @Type Int HiddenAge } at [TermDeclaration(Idx::(0))] -CoercibleConstructorNotInScope { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(1))] -NoInstanceFound { Coercible @Type Int HiddenAge } at [TermDeclaration(Idx::(1))] +Diagnostics +error[CoercibleConstructorNotInScope] at 85..119: Constructor not in scope for Coercible +error[NoInstanceFound] at 85..119: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope] at 141..180: Constructor not in scope for Coercible +error[NoInstanceFound] at 141..180: No instance found for: Coercible @Type Int HiddenAge diff --git a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap index cffd0690..75ca5996 100644 --- a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap +++ b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap @@ -7,6 +7,6 @@ coerceOpen :: Int -> HiddenAge Types -Errors -CoercibleConstructorNotInScope { file_id: Idx::(30), item_id: Idx::(0) } at [TermDeclaration(Idx::(0))] -NoInstanceFound { Coercible @Type Int HiddenAge } at [TermDeclaration(Idx::(0))] +Diagnostics +error[CoercibleConstructorNotInScope] at 57..89: Constructor not in scope for Coercible +error[NoInstanceFound] at 57..89: No instance found for: Coercible @Type Int HiddenAge diff --git a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap index 541c5dcc..7fe6b950 100644 --- a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap +++ b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap @@ -29,5 +29,5 @@ Maybe = [Representational] List = [Representational] Container = [Representational] -Errors -NoInstanceFound { Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) } at [TermDeclaration(Idx::(4))] +Diagnostics +error[NoInstanceFound] at 209..272: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap index f6f770a2..14cb0f01 100644 --- a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap +++ b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap @@ -1,6 +1,5 @@ --- source: tests-integration/tests/checking/generated.rs -assertion_line: 12 expression: report --- Terms @@ -12,11 +11,11 @@ invalidEq :: Proxy @(Row Int) ( left :: 1, right :: 5 ) Types -Errors -NoInstanceFound { Compare 10 5 LT } at [TermDeclaration(Idx::(0))] -NoInstanceFound { Compare 5 1 LT } at [TermDeclaration(Idx::(0))] -NoInstanceFound { Compare 1 5 GT } at [TermDeclaration(Idx::(1))] -NoInstanceFound { Compare 5 10 GT } at [TermDeclaration(Idx::(1))] -NoInstanceFound { Compare 5 1 LT } at [TermDeclaration(Idx::(2))] -NoInstanceFound { Compare 1 5 GT } at [TermDeclaration(Idx::(3))] -NoInstanceFound { Compare 1 5 EQ } at [TermDeclaration(Idx::(4))] +Diagnostics +error[NoInstanceFound] at 49..182: No instance found for: Compare 10 5 LT +error[NoInstanceFound] at 49..182: No instance found for: Compare 5 1 LT +error[NoInstanceFound] at 226..359: No instance found for: Compare 1 5 GT +error[NoInstanceFound] at 226..359: No instance found for: Compare 5 10 GT +error[NoInstanceFound] at 403..488: No instance found for: Compare 5 1 LT +error[NoInstanceFound] at 513..559: No instance found for: Compare 1 5 GT +error[NoInstanceFound] at 585..631: No instance found for: Compare 1 5 EQ diff --git a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap index d4173f18..bc4a790e 100644 --- a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap +++ b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap @@ -47,21 +47,13 @@ Proxy Roles Proxy = [Phantom] -Errors -CustomWarning { .. } at [TermDeclaration(Idx::(2))] - This function is deprecated -CustomWarning { .. } at [TermDeclaration(Idx::(4))] - Left Right -CustomWarning { .. } at [TermDeclaration(Idx::(6))] - Line 1 - Line 2 -CustomWarning { .. } at [TermDeclaration(Idx::(8))] - Got type: Int -CustomWarning { .. } at [TermDeclaration(Idx::(10))] - Label: myField -CustomWarning { .. } at [TermDeclaration(Idx::(12))] - Label: "h e l l o" -CustomWarning { .. } at [TermDeclaration(Idx::(14))] - Label: "hel\"lo" -CustomWarning { .. } at [TermDeclaration(Idx::(16))] - Label: """raw\nstring""" +Diagnostics +warning[CustomWarning] at 241..262: This function is deprecated +warning[CustomWarning] at 386..408: Left Right +warning[CustomWarning] at 533..554: Line 1 +Line 2 +warning[CustomWarning] at 688..715: Got type: Int +warning[CustomWarning] at 860..886: Label: myField +warning[CustomWarning] at 1052..1084: Label: "h e l l o" +warning[CustomWarning] at 1258..1289: Label: "hel\"lo" +warning[CustomWarning] at 1465..1494: Label: """raw\nstring""" diff --git a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap index 559b6812..e5be9930 100644 --- a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap +++ b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap @@ -26,9 +26,7 @@ Proxy Roles Proxy = [Phantom] -Errors -CustomFailure { .. } at [TermDeclaration(Idx::(2))] - This operation is not allowed -CustomFailure { .. } at [TermDeclaration(Idx::(4))] - Error: - Type String +Diagnostics +error[CustomFailure] at 231..252: This operation is not allowed +error[CustomFailure] at 409..441: Error: +Type String diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 9c8e38d8..f8a38cb0 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; +use diagnostics::{DiagnosticsContext, ToDiagnostics, format_text}; use files::FileId; use indexing::{ImportKind, TermItem, TypeItem, TypeItemId, TypeItemKind}; use lowering::{ @@ -382,94 +383,34 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { writeln!(snapshot, "derive {forall_prefix}{head}").unwrap(); } - if !checked.errors.is_empty() { - writeln!(snapshot, "\nErrors").unwrap(); + let content = engine.content(id); + + let (parsed, _) = engine.parsed(id).unwrap(); + let root = parsed.syntax_node(); + + let stabilized = engine.stabilized(id).unwrap(); + let lowered = engine.lowered(id).unwrap(); + let resolved = engine.resolved(id).unwrap(); + + let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &checked); + + let mut all_diagnostics = vec![]; + + for error in &lowered.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } + + for error in &resolved.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); } + for error in &checked.errors { - use checking::error::ErrorKind::*; - let message = |id| &checked.error_messages[id]; - let step = &error.step; - match error.kind { - CannotUnify { t1, t2 } => { - writeln!( - snapshot, - "CannotUnify {{ {}, {} }} at {step:?}", - message(t1), - message(t2) - ) - .unwrap(); - } - NoInstanceFound { constraint } => { - writeln!(snapshot, "NoInstanceFound {{ {} }} at {step:?}", message(constraint)) - .unwrap(); - } - AmbiguousConstraint { constraint } => { - writeln!(snapshot, "AmbiguousConstraint {{ {} }} at {step:?}", message(constraint)) - .unwrap(); - } - InstanceMemberTypeMismatch { expected, actual } => { - writeln!( - snapshot, - "InstanceMemberTypeMismatch {{ expected: {}, actual: {} }} at {step:?}", - message(expected), - message(actual) - ) - .unwrap(); - } - CustomWarning { message_id } => { - let message = message(message_id); - writeln!(snapshot, "CustomWarning {{ .. }} at {step:?}").unwrap(); - for line in message.lines() { - writeln!(snapshot, " {line}").unwrap(); - } - } - CustomFailure { message_id } => { - let message = message(message_id); - writeln!(snapshot, "CustomFailure {{ .. }} at {step:?}").unwrap(); - for line in message.lines() { - writeln!(snapshot, " {line}").unwrap(); - } - } - CannotDeriveForType { type_message } => { - writeln!( - snapshot, - "CannotDeriveForType {{ {} }} at {step:?}", - message(type_message) - ) - .unwrap(); - } - ExpectedNewtype { type_message } => { - writeln!(snapshot, "ExpectedNewtype {{ {} }} at {step:?}", message(type_message)) - .unwrap(); - } - CovariantOccurrence { type_message } => { - writeln!( - snapshot, - "CovariantOccurrence {{ {} }} at {step:?}", - message(type_message) - ) - .unwrap(); - } - ContravariantOccurrence { type_message } => { - writeln!( - snapshot, - "ContravariantOccurrence {{ {} }} at {step:?}", - message(type_message) - ) - .unwrap(); - } - InvalidTypeOperator { kind_message } => { - writeln!( - snapshot, - "InvalidTypeOperator {{ {} }} at {step:?}", - message(kind_message) - ) - .unwrap(); - } - _ => { - writeln!(snapshot, "{:?} at {step:?}", error.kind).unwrap(); - } - } + all_diagnostics.extend(error.to_diagnostics(&context)); + } + + if !all_diagnostics.is_empty() { + writeln!(snapshot, "\nDiagnostics").unwrap(); + snapshot.push_str(&format_text(&all_diagnostics)); } snapshot From 11fb8c6e52518e05fff3d48de9f8c2b32b882073 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 00:28:04 +0800 Subject: [PATCH 004/386] Experiments with incremental disclosure --- AGENTS.md | 82 +++++----------------- compiler-core/AGENTS.md | 30 ++++++++ compiler-core/CLAUDE.md | 1 + compiler-scripts/src/bin/test-checking.rs | 85 +++++++++++++++++++---- 4 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 compiler-core/AGENTS.md create mode 120000 compiler-core/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 797ea6ac..100e299b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,71 +1,27 @@ -## Project Overview - -purescript-analyzer is a compiler frontend for the PureScript programming language written -in Rust. It provides additional LSP server functionality for the language. The compiler is -organised as a Cargo workspace with functionality located in separate folders. - -### Compiler Core - -The compiler core is split into different core components in `./compiler-core`, with each -components having well-defined responsibilities. These components are designed to produce -information as transparent as possible, such that editor integrations can be built around -introspection. Likewise, these components are also designed to be compatible with a -query-based and incremental build system. - -- checking: type checking and elaboration -- indexing: high-level relationships between module items -- lexing: tokenization and the layout algorithm -- lowering: core semantic representation, name resolution -- parsing: parsing into a rowan-based CST -- resolving: name-indexed interface for module items -- stabilizing: assigns stable IDs to source ranges -- sugar: syntax desugaring such as operator bracketing -- syntax: types for the rowan-based CST - -Additionally, the following crates are related to the build system implementation. +## Commands + +```bash +cargo check -p --tests # Type check a crate (always specify -p) +cargo nextest run -p # Run all tests in a crate +cargo nextest run -p # Run single test +just tc # Type checker integration tests +just tc 101 # Run specific test (by filter) +just fix # Apply clippy fixes and format +``` -- building: query-based parallel build system -- building-types: shared type definitions -- files: virtual file system -- interner: generic interner implementation +## Architecture -### LSP and Binary +PureScript compiler frontend in Rust using rowan (lossless syntax trees) and query-based incremental builds. -- `./compiler-bin`: implements the `purescript-analyzer` executable -- `./compiler-lsp`: LSP server functionality used in `./compiler-bin` +**compiler-core/**: checking (types), indexing, lexing, lowering, parsing, resolving, stabilizing, sugar, syntax, building, files, interner +**compiler-bin/**: CLI executable | **compiler-lsp/**: LSP server -## Key Concepts +## Code Style -Additional concepts that you should be mindful of, the compiler: -- uses rust-analyzer/rowan, a lossless syntax tree library inspired by Swift's libsyntax -- uses a query-based incremental build system rather than a traditional phase-based setup -- uses techniques like interning and arena allocation to enable better caching patterns - - for instance, whitespace does not immediately invalidate type checking results +- Use `cargo fmt` (rustfmt with `use_small_heuristics = "Max"`) +- Use `just format-imports` for module-granularity imports (requires nightly) +- Leverage interning/arena allocation for caching; avoid unnecessary allocations ## Skills -Agent skills are specialized instruction sets for common tasks. They're stored in `.claude/skills/`. - -- **type-checker-tests**: Use this when asked to implement tests for type checker inference and checking - -## Quick Commands - -Always provide the crate name of the code that you're working on for efficiency. - -``` -# Bread and butter, check for errors -cargo check -p --tests - -# Run tests (always use nextest) -cargo nextest run -p -``` - -### Domain Specific - -``` -# Run type checker integration tests -just tc - -# You can also provide a test filter -just tc 101 -``` +Load `.claude/skills/type-checker-tests` when implementing type checker tests. diff --git a/compiler-core/AGENTS.md b/compiler-core/AGENTS.md new file mode 100644 index 00000000..3011ef90 --- /dev/null +++ b/compiler-core/AGENTS.md @@ -0,0 +1,30 @@ +## Architecture + +The compiler core is split into components with well-defined responsibilities, designed for +transparency (editor introspection) and compatibility with query-based incremental builds. + +### Pipeline Components + +- **lexing**: tokenization and the layout algorithm +- **parsing**: parsing into a rowan-based CST +- **syntax**: types for the rowan-based CST +- **sugar**: syntax desugaring (e.g., operator bracketing) +- **lowering**: core semantic representation, name resolution +- **indexing**: high-level relationships between module items +- **resolving**: name-indexed interface for module items +- **stabilizing**: assigns stable IDs to source ranges +- **checking**: type checking and elaboration + +### Infrastructure + +- **building**: query-based parallel build system +- **building-types**: shared type definitions +- **files**: virtual file system +- **interner**: generic interner implementation +- **prim-constants**: primitive type constants + +## Key Concepts + +- Uses rust-analyzer/rowan, a lossless syntax tree library inspired by Swift's libsyntax +- Query-based incremental builds (not traditional phase-based) +- Interning and arena allocation enable better caching (e.g., whitespace changes don't invalidate type checking) diff --git a/compiler-core/CLAUDE.md b/compiler-core/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/compiler-core/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/compiler-scripts/src/bin/test-checking.rs b/compiler-scripts/src/bin/test-checking.rs index ab4ec034..3763c364 100644 --- a/compiler-scripts/src/bin/test-checking.rs +++ b/compiler-scripts/src/bin/test-checking.rs @@ -33,16 +33,25 @@ struct Config { debug: bool, } +struct TestOutcome { + tests_passed: bool, + pending_count: usize, + trace_paths: Vec, +} + fn main() { let config = Config::parse(); let (fixture_hashes, hash_duration) = hash_fixtures(); println!("{}", style(format!("Hashed fixtures in {}ms", hash_duration.as_millis())).dim()); - run_tests(&config, &fixture_hashes); + let tests_passed = run_tests(&config, &fixture_hashes); let trace_paths = collect_trace_paths(&config); - process_pending_snapshots(&config, &trace_paths); + let pending_count = process_pending_snapshots(&config, &trace_paths); + + let outcome = TestOutcome { tests_passed, pending_count, trace_paths }; + print_next_actions(&config, &outcome); } fn hash_fixtures() -> (HashMap, Duration) { @@ -78,11 +87,12 @@ fn build_nextest_command(config: &Config, fixture_hashes: &HashMap) { +fn run_tests(config: &Config, fixture_hashes: &HashMap) -> bool { let mut cmd = build_nextest_command(config, fixture_hashes); if config.verbose { - cmd.status().expect("Failed to run cargo nextest"); + let status = cmd.status().expect("Failed to run cargo nextest"); + status.success() } else { cmd.stdout(Stdio::null()).stderr(Stdio::null()); let status = cmd.status().expect("Failed to run cargo nextest"); @@ -95,6 +105,8 @@ fn run_tests(config: &Config, fixture_hashes: &HashMap) { let mut retry = build_nextest_command(&verbose_config, fixture_hashes); let _ = retry.status(); } + + status.success() } } @@ -164,7 +176,7 @@ fn find_trace_for_snapshot(snap_path: &str, trace_paths: &[PathBuf]) -> Option

usize { let pending_output = Command::new("cargo") .arg("insta") .arg("pending-snapshots") @@ -176,15 +188,9 @@ fn process_pending_snapshots(config: &Config, trace_paths: &[PathBuf]) { let pending = String::from_utf8_lossy(&pending_output.stdout); let pending = pending.trim(); - if pending.is_empty() { - println!("{}", style("No pending snapshots.").dim()); - return; - } - - println!(); - let cwd = env::current_dir().unwrap(); + let mut count = 0; for line in pending.lines() { let line = line.trim(); if line.is_empty() { @@ -217,8 +223,11 @@ fn process_pending_snapshots(config: &Config, trace_paths: &[PathBuf]) { display_new_snapshot(&snap_new, short_path, trace_path.as_deref()); } + count += 1; println!(); } + + count } fn display_snapshot_diff( @@ -227,6 +236,7 @@ fn display_snapshot_diff( short_path: &str, trace_path: Option<&Path>, ) { + println!(); println!("{} {}", style("UPDATED").yellow().bold(), style(short_path).cyan()); if let Some(trace) = trace_path { @@ -258,3 +268,54 @@ fn display_new_snapshot(snap_new: &Path, short_path: &str, trace_path: Option<&P println!("{} {}", style(format!("{:3}", i + 1)).dim(), line); } } + +fn print_next_actions(config: &Config, outcome: &TestOutcome) { + let filters_str = if config.filters.is_empty() { + String::new() + } else { + format!(" {}", config.filters.join(" ")) + }; + + if outcome.tests_passed && outcome.pending_count == 0 { + println!("{}", style("No pending snapshots.").green()); + return; + } + + if outcome.pending_count > 0 { + println!("{}", style("-".repeat(60)).dim()); + println!(); + println!( + "{} pending snapshot{}", + outcome.pending_count, + if outcome.pending_count == 1 { "" } else { "s" } + ); + println!(); + println!(" If this is expected, run {}", style("cargo insta accept").cyan()); + println!(" If this is not expected, run {}", style("cargo insta reject").cyan()); + println!(); + } + + if !outcome.tests_passed { + println!("{}", style("-".repeat(60)).dim()); + println!(); + println!("{}", style("Tests failed, consider consulting trace files.").red(),); + if !config.debug { + println!( + "Enable debug tracing for more information: {}", + style(format!("just tc --debug{}", filters_str)).cyan() + ); + } + if !outcome.trace_paths.is_empty() { + let maximum_shown = 10; + for trace in outcome.trace_paths.iter().take(maximum_shown) { + println!("{} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); + } + if outcome.trace_paths.len() > maximum_shown { + let additional = outcome.trace_paths.len() - maximum_shown; + let additional = style(format!("An additional {additional} is stored in target/compiler-tracing")).dim(); + println!("{additional}"); + } + } + println!(); + } +} From 134c79f8276442e2e0a87230a5f4b0162e4afc8c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 00:45:22 +0800 Subject: [PATCH 005/386] Unify scripts for running tests --- .claude/skills/type-checker-tests/SKILL.md | 18 +- compiler-core/diagnostics/src/context.rs | 3 +- compiler-scripts/Cargo.toml | 4 +- compiler-scripts/src/bin/test-checking.rs | 321 ------------------- compiler-scripts/src/lib.rs | 2 + compiler-scripts/src/main.rs | 21 ++ compiler-scripts/src/test_runner.rs | 48 +++ compiler-scripts/src/test_runner/category.rs | 55 ++++ compiler-scripts/src/test_runner/cli.rs | 16 + compiler-scripts/src/test_runner/nextest.rs | 70 ++++ compiler-scripts/src/test_runner/pending.rs | 76 +++++ compiler-scripts/src/test_runner/traces.rs | 62 ++++ compiler-scripts/src/test_runner/ui.rs | 98 ++++++ justfile | 8 +- 14 files changed, 466 insertions(+), 336 deletions(-) delete mode 100644 compiler-scripts/src/bin/test-checking.rs create mode 100644 compiler-scripts/src/main.rs create mode 100644 compiler-scripts/src/test_runner.rs create mode 100644 compiler-scripts/src/test_runner/category.rs create mode 100644 compiler-scripts/src/test_runner/cli.rs create mode 100644 compiler-scripts/src/test_runner/nextest.rs create mode 100644 compiler-scripts/src/test_runner/pending.rs create mode 100644 compiler-scripts/src/test_runner/traces.rs create mode 100644 compiler-scripts/src/test_runner/ui.rs diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index cacaeb82..b7b82086 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -15,12 +15,12 @@ Use this skill when adding new type checker functions or expanding behavior. | Action | Command | |--------|---------| | Find next test number | `ls tests-integration/fixtures/checking/ \| tail -5` | -| Run a test or multiple tests | `just tc NNN` or `just tc 101 102` | -| Run with tracing enabled | `just tc --debug NNN` | -| Run all checking tests | `just tc` | +| Run a test or multiple tests | `just t checking NNN` or `just tc 101 102` | +| Run with tracing enabled | `just t checking --debug NNN` | +| Run all checking tests | `just t checking` or `just tc` | | Accept all pending snapshots | `cargo insta accept` | -Use `just tc --help` for all options. +Use `just t checking --help` for all options. ## Creating a Test @@ -55,7 +55,7 @@ test' [x] = x ### 3. Generate and review snapshot ```bash -just tc NNN +just t checking NNN ``` This outputs: @@ -153,7 +153,7 @@ ErrorKind { details } at [location] cargo insta accept # Verify all checking tests pass -just tc +just t checking ``` ## Debugging @@ -162,10 +162,10 @@ When investigating a potential compiler bug: ```bash # Focus on single test to reduce noise -just tc NNN +just t checking NNN # Enable tracing to see type checker behaviour -just tc --debug NNN +just t checking --debug NNN ``` ### Trace Files @@ -214,4 +214,4 @@ jq 'select(.target | contains("unification"))' target/compiler-tracing/NNN_*.jso jq '{level, target, fields}' target/compiler-tracing/NNN_*.jsonl ``` -You should run `just tc` to check for regressions. +You should run `just t checking` to check for regressions. diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index fc39aca6..a34ae3ec 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -1,4 +1,5 @@ -use checking::{CheckedModule, error::ErrorStep}; +use checking::CheckedModule; +use checking::error::ErrorStep; use indexing::IndexedModule; use rowan::ast::{AstNode, AstPtr}; use stabilizing::StabilizedModule; diff --git a/compiler-scripts/Cargo.toml b/compiler-scripts/Cargo.toml index 6880cb04..ed235447 100644 --- a/compiler-scripts/Cargo.toml +++ b/compiler-scripts/Cargo.toml @@ -12,6 +12,4 @@ similar = { version = "2", features = ["inline"] } md-5 = "0.10" walkdir = "2" -[[bin]] -name = "test-checking" -path = "src/bin/test-checking.rs" + diff --git a/compiler-scripts/src/bin/test-checking.rs b/compiler-scripts/src/bin/test-checking.rs deleted file mode 100644 index 3763c364..00000000 --- a/compiler-scripts/src/bin/test-checking.rs +++ /dev/null @@ -1,321 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::time::{Duration, Instant}; -use std::{env, fs}; - -use clap::Parser; -use compiler_scripts::console::style; -use compiler_scripts::fixtures::fixture_env; -use compiler_scripts::snapshots::{print_diff, strip_frontmatter}; -use serde::Deserialize; - -const TRACE_DIR: &str = "target/compiler-tracing"; - -#[derive(Deserialize)] -struct PendingSnapshot { - path: String, -} - -#[derive(Parser)] -#[command(about = "Run type checker integration tests with snapshot diffing")] -struct Config { - /// Test name or number filters - #[arg(num_args = 0..)] - filters: Vec, - - /// Verbose output (show test progress) - #[arg(short, long)] - verbose: bool, - - /// Enable tracing output for debugging - #[arg(long)] - debug: bool, -} - -struct TestOutcome { - tests_passed: bool, - pending_count: usize, - trace_paths: Vec, -} - -fn main() { - let config = Config::parse(); - - let (fixture_hashes, hash_duration) = hash_fixtures(); - println!("{}", style(format!("Hashed fixtures in {}ms", hash_duration.as_millis())).dim()); - - let tests_passed = run_tests(&config, &fixture_hashes); - - let trace_paths = collect_trace_paths(&config); - let pending_count = process_pending_snapshots(&config, &trace_paths); - - let outcome = TestOutcome { tests_passed, pending_count, trace_paths }; - print_next_actions(&config, &outcome); -} - -fn hash_fixtures() -> (HashMap, Duration) { - let start = Instant::now(); - let hashes = fixture_env(); - (hashes, start.elapsed()) -} - -fn build_nextest_command(config: &Config, fixture_hashes: &HashMap) -> Command { - let mut cmd = Command::new("cargo"); - cmd.arg("nextest").arg("run").arg("-p").arg("tests-integration").arg("--test").arg("checking"); - - if config.debug { - cmd.env("TRACE_LEVEL", "debug"); - } - - for filter in &config.filters { - cmd.arg(filter); - } - - if config.verbose { - cmd.arg("--status-level=fail"); - cmd.arg("--color=always"); - } else { - cmd.arg("--status-level=none"); - } - - cmd.env("INSTA_FORCE_PASS", "1"); - for (key, value) in fixture_hashes { - cmd.env(key, value); - } - - cmd -} - -fn run_tests(config: &Config, fixture_hashes: &HashMap) -> bool { - let mut cmd = build_nextest_command(config, fixture_hashes); - - if config.verbose { - let status = cmd.status().expect("Failed to run cargo nextest"); - status.success() - } else { - cmd.stdout(Stdio::null()).stderr(Stdio::null()); - let status = cmd.status().expect("Failed to run cargo nextest"); - - if !status.success() { - eprintln!("{}", style("Tests failed, re-running verbose...").yellow()); - - let verbose_config = - Config { filters: config.filters.clone(), verbose: true, debug: config.debug }; - let mut retry = build_nextest_command(&verbose_config, fixture_hashes); - let _ = retry.status(); - } - - status.success() - } -} - -fn collect_trace_paths(config: &Config) -> Vec { - if !config.debug { - return Vec::new(); - } - - let trace_dir = PathBuf::from(TRACE_DIR); - if !trace_dir.exists() { - return Vec::new(); - } - - let Ok(entries) = fs::read_dir(&trace_dir) else { - return Vec::new(); - }; - - let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); - entries.sort_by_key(|e| e.path()); - - let mut trace_paths = Vec::new(); - for entry in entries { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "jsonl") { - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - - // Skip traces that don't match any filter - if !config.filters.is_empty() && !config.filters.iter().any(|f| file_name.contains(f)) { - continue; - } - - trace_paths.push(path); - } - } - - trace_paths -} - -/// Finds a trace file that matches the given snapshot path. -/// -/// Snapshot paths look like: `.../fixtures/checking/200_int_compare_transitive/Main.snap` -/// Trace files look like: `200_int_compare_transitive_Main.jsonl` -/// -/// We extract the test identifier (e.g., `200_int_compare_transitive`) from the snapshot's -/// parent directory and the module name from the file, then find a matching trace file. -fn find_trace_for_snapshot(snap_path: &str, trace_paths: &[PathBuf]) -> Option { - let path = Path::new(snap_path); - - // Get module name from file (e.g., "Main" from "Main.snap") - let module_name = path.file_stem()?.to_str()?; - - // Get test identifier from parent directory (e.g., "200_int_compare_transitive") - let test_id = path.parent()?.file_name()?.to_str()?; - - // Trace files are named: {test_id}_{module_name}.jsonl - let expected_trace_name = format!("{}_{}.jsonl", test_id, module_name); - - // Find matching trace file - trace_paths - .iter() - .find(|trace_path| { - trace_path - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name == expected_trace_name) - }) - .cloned() -} - -fn process_pending_snapshots(config: &Config, trace_paths: &[PathBuf]) -> usize { - let pending_output = Command::new("cargo") - .arg("insta") - .arg("pending-snapshots") - .arg("--as-json") - .stderr(Stdio::null()) - .output() - .expect("Failed to run cargo insta"); - - let pending = String::from_utf8_lossy(&pending_output.stdout); - let pending = pending.trim(); - - let cwd = env::current_dir().unwrap(); - - let mut count = 0; - for line in pending.lines() { - let line = line.trim(); - if line.is_empty() { - continue; - } - - let snapshot_path = if let Ok(snapshot) = serde_json::from_str::(line) { - snapshot.path - } else { - continue; - }; - - // Skip snapshots that don't match any filter - if !config.filters.is_empty() && !config.filters.iter().any(|f| snapshot_path.contains(f)) { - continue; - } - - let short_path = snapshot_path - .strip_prefix(cwd.to_str().unwrap_or("")) - .unwrap_or(&snapshot_path) - .trim_start_matches('/'); - - let snap = Path::new(&snapshot_path); - let snap_new = PathBuf::from(format!("{}.new", snapshot_path)); - let trace_path = find_trace_for_snapshot(&snapshot_path, trace_paths); - - if snap.exists() { - display_snapshot_diff(snap, &snap_new, short_path, trace_path.as_deref()); - } else { - display_new_snapshot(&snap_new, short_path, trace_path.as_deref()); - } - - count += 1; - println!(); - } - - count -} - -fn display_snapshot_diff( - snap: &Path, - snap_new: &Path, - short_path: &str, - trace_path: Option<&Path>, -) { - println!(); - println!("{} {}", style("UPDATED").yellow().bold(), style(short_path).cyan()); - - if let Some(trace) = trace_path { - println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); - } - - println!(); - - let old_content = fs::read_to_string(snap).unwrap_or_default(); - let new_content = fs::read_to_string(snap_new).unwrap_or_default(); - - let old_stripped = strip_frontmatter(&old_content); - let new_stripped = strip_frontmatter(&new_content); - - print_diff(old_stripped, new_stripped); -} - -fn display_new_snapshot(snap_new: &Path, short_path: &str, trace_path: Option<&Path>) { - println!("{} {}", style("CREATED").green().bold(), style(short_path).cyan()); - - if let Some(trace) = trace_path { - println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); - } - - println!(); - - let new_content = fs::read_to_string(snap_new).unwrap_or_default(); - for (i, line) in strip_frontmatter(&new_content).lines().enumerate() { - println!("{} {}", style(format!("{:3}", i + 1)).dim(), line); - } -} - -fn print_next_actions(config: &Config, outcome: &TestOutcome) { - let filters_str = if config.filters.is_empty() { - String::new() - } else { - format!(" {}", config.filters.join(" ")) - }; - - if outcome.tests_passed && outcome.pending_count == 0 { - println!("{}", style("No pending snapshots.").green()); - return; - } - - if outcome.pending_count > 0 { - println!("{}", style("-".repeat(60)).dim()); - println!(); - println!( - "{} pending snapshot{}", - outcome.pending_count, - if outcome.pending_count == 1 { "" } else { "s" } - ); - println!(); - println!(" If this is expected, run {}", style("cargo insta accept").cyan()); - println!(" If this is not expected, run {}", style("cargo insta reject").cyan()); - println!(); - } - - if !outcome.tests_passed { - println!("{}", style("-".repeat(60)).dim()); - println!(); - println!("{}", style("Tests failed, consider consulting trace files.").red(),); - if !config.debug { - println!( - "Enable debug tracing for more information: {}", - style(format!("just tc --debug{}", filters_str)).cyan() - ); - } - if !outcome.trace_paths.is_empty() { - let maximum_shown = 10; - for trace in outcome.trace_paths.iter().take(maximum_shown) { - println!("{} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); - } - if outcome.trace_paths.len() > maximum_shown { - let additional = outcome.trace_paths.len() - maximum_shown; - let additional = style(format!("An additional {additional} is stored in target/compiler-tracing")).dim(); - println!("{additional}"); - } - } - println!(); - } -} diff --git a/compiler-scripts/src/lib.rs b/compiler-scripts/src/lib.rs index 7792dc5f..0ab89a1a 100644 --- a/compiler-scripts/src/lib.rs +++ b/compiler-scripts/src/lib.rs @@ -1,5 +1,7 @@ pub use console; +pub mod test_runner; + pub mod fixtures { use md5::{Digest, Md5}; use std::collections::HashMap; diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs new file mode 100644 index 00000000..96862b1c --- /dev/null +++ b/compiler-scripts/src/main.rs @@ -0,0 +1,21 @@ +use clap::Parser; +use compiler_scripts::test_runner::{RunArgs, TestCategory, run_category}; + +#[derive(Parser)] +#[command(about = "Compiler development scripts")] +struct Cli { + /// Test category: checking (c), lowering (l), resolving (r), lsp + category: TestCategory, + + #[command(flatten)] + args: RunArgs, +} + +fn main() { + let cli = Cli::parse(); + let outcome = run_category(cli.category, &cli.args); + + if !outcome.tests_passed { + std::process::exit(1); + } +} diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs new file mode 100644 index 00000000..3c044026 --- /dev/null +++ b/compiler-scripts/src/test_runner.rs @@ -0,0 +1,48 @@ +mod category; +mod cli; +mod nextest; +mod pending; +mod traces; +mod ui; + +pub use category::TestCategory; +pub use cli::RunArgs; + +use std::path::PathBuf; +use std::time::Instant; + +use console::style; + +pub struct TestOutcome { + pub tests_passed: bool, + pub pending_count: usize, + pub trace_paths: Vec, +} + +pub fn run_category(category: TestCategory, args: &RunArgs) -> TestOutcome { + // 1. Hash fixtures and print timing + let start = Instant::now(); + let fixture_hashes = crate::fixtures::fixture_env(); + println!("{}", style(format!("Hashed fixtures in {}ms", start.elapsed().as_millis())).dim()); + + // 2. Run nextest + let tests_passed = nextest::run_nextest(category, args, &fixture_hashes); + + // 3. Collect trace paths + let trace_paths = traces::collect_trace_paths(&args.filters, args.debug); + + // 4. Process pending snapshots + let pending_count = pending::process_pending_snapshots(category, args, &trace_paths); + + // 5. Print next actions + ui::print_next_actions( + category.as_str(), + &args.filters, + tests_passed, + pending_count, + &trace_paths, + args.debug, + ); + + TestOutcome { tests_passed, pending_count, trace_paths } +} diff --git a/compiler-scripts/src/test_runner/category.rs b/compiler-scripts/src/test_runner/category.rs new file mode 100644 index 00000000..10071d3e --- /dev/null +++ b/compiler-scripts/src/test_runner/category.rs @@ -0,0 +1,55 @@ +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +#[derive(Copy, Clone, Debug)] +pub enum TestCategory { + Checking, + Lowering, + Resolving, + Lsp, +} + +impl TestCategory { + pub fn as_str(&self) -> &'static str { + match self { + TestCategory::Checking => "checking", + TestCategory::Lowering => "lowering", + TestCategory::Resolving => "resolving", + TestCategory::Lsp => "lsp", + } + } + + pub fn fixtures_subdir_fragment(&self) -> String { + format!("tests-integration/fixtures/{}", self.as_str()) + } + + pub fn extra_env(&self, debug: bool) -> Vec<(&'static str, String)> { + if debug { vec![("TRACE_LEVEL", "debug".to_string())] } else { vec![] } + } + + pub fn trace_for_snapshot(&self, snap_path: &Path, trace_paths: &[PathBuf]) -> Option { + match self { + TestCategory::Checking => { + crate::test_runner::traces::match_checking_trace(snap_path, trace_paths) + } + _ => None, + } + } +} + +impl FromStr for TestCategory { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "checking" | "c" => Ok(TestCategory::Checking), + "lowering" | "l" => Ok(TestCategory::Lowering), + "resolving" | "r" => Ok(TestCategory::Resolving), + "lsp" => Ok(TestCategory::Lsp), + _ => Err(format!( + "unknown test category '{}', expected: checking (c), lowering (l), resolving (r), lsp", + s + )), + } + } +} diff --git a/compiler-scripts/src/test_runner/cli.rs b/compiler-scripts/src/test_runner/cli.rs new file mode 100644 index 00000000..8ffa86b4 --- /dev/null +++ b/compiler-scripts/src/test_runner/cli.rs @@ -0,0 +1,16 @@ +use clap::Args; + +#[derive(Args, Clone, Debug)] +pub struct RunArgs { + /// Test name or number filters (passed through to nextest) + #[arg(num_args = 0..)] + pub filters: Vec, + + /// Verbose output (show test progress) + #[arg(short, long)] + pub verbose: bool, + + /// Enable tracing output for debugging + #[arg(long)] + pub debug: bool, +} diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs new file mode 100644 index 00000000..a960e204 --- /dev/null +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; +use std::process::{Command, Stdio}; + +use console::style; + +use crate::test_runner::category::TestCategory; +use crate::test_runner::cli::RunArgs; + +pub fn build_nextest_command( + category: TestCategory, + args: &RunArgs, + fixture_hashes: &HashMap, +) -> Command { + let mut cmd = Command::new("cargo"); + cmd.arg("nextest") + .arg("run") + .arg("-p") + .arg("tests-integration") + .arg("--test") + .arg(category.as_str()); + + for (key, value) in category.extra_env(args.debug) { + cmd.env(key, value); + } + + for filter in &args.filters { + cmd.arg(filter); + } + + if args.verbose { + cmd.arg("--status-level=fail"); + cmd.arg("--color=always"); + } else { + cmd.arg("--status-level=none"); + } + + cmd.env("INSTA_FORCE_PASS", "1"); + for (key, value) in fixture_hashes { + cmd.env(key, value); + } + + cmd +} + +pub fn run_nextest( + category: TestCategory, + args: &RunArgs, + fixture_hashes: &HashMap, +) -> bool { + let mut cmd = build_nextest_command(category, args, fixture_hashes); + + if args.verbose { + let status = cmd.status().expect("Failed to run cargo nextest"); + status.success() + } else { + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + let status = cmd.status().expect("Failed to run cargo nextest"); + + if !status.success() { + eprintln!("{}", style("Tests failed, re-running verbose...").yellow()); + + let verbose_args = + RunArgs { filters: args.filters.clone(), verbose: true, debug: args.debug }; + let mut retry = build_nextest_command(category, &verbose_args, fixture_hashes); + let _ = retry.status(); + } + + status.success() + } +} diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs new file mode 100644 index 00000000..f0a5da9c --- /dev/null +++ b/compiler-scripts/src/test_runner/pending.rs @@ -0,0 +1,76 @@ +use std::env; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use serde::Deserialize; + +use crate::test_runner::category::TestCategory; +use crate::test_runner::cli::RunArgs; +use crate::test_runner::ui; + +#[derive(Deserialize)] +struct PendingSnapshot { + path: String, +} + +pub fn process_pending_snapshots( + category: TestCategory, + args: &RunArgs, + trace_paths: &[PathBuf], +) -> usize { + let pending_output = Command::new("cargo") + .arg("insta") + .arg("pending-snapshots") + .arg("--as-json") + .stderr(Stdio::null()) + .output() + .expect("Failed to run cargo insta"); + + let pending = String::from_utf8_lossy(&pending_output.stdout); + let pending = pending.trim(); + + let cwd = env::current_dir().unwrap(); + let fixtures_fragment = category.fixtures_subdir_fragment(); + + let mut count = 0; + for line in pending.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let snapshot_path = if let Ok(snapshot) = serde_json::from_str::(line) { + snapshot.path + } else { + continue; + }; + + if !snapshot_path.contains(&fixtures_fragment) { + continue; + } + + if !args.filters.is_empty() && !args.filters.iter().any(|f| snapshot_path.contains(f)) { + continue; + } + + let short_path = snapshot_path + .strip_prefix(cwd.to_str().unwrap_or("")) + .unwrap_or(&snapshot_path) + .trim_start_matches('/'); + + let snap = Path::new(&snapshot_path); + let snap_new = PathBuf::from(format!("{}.new", snapshot_path)); + let trace_path = category.trace_for_snapshot(snap, trace_paths); + + if snap.exists() { + ui::display_snapshot_diff(snap, &snap_new, short_path, trace_path.as_deref()); + } else { + ui::display_new_snapshot(&snap_new, short_path, trace_path.as_deref()); + } + + count += 1; + println!(); + } + + count +} diff --git a/compiler-scripts/src/test_runner/traces.rs b/compiler-scripts/src/test_runner/traces.rs new file mode 100644 index 00000000..4e2bc888 --- /dev/null +++ b/compiler-scripts/src/test_runner/traces.rs @@ -0,0 +1,62 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +pub const TRACE_DIR: &str = "target/compiler-tracing"; + +pub fn collect_trace_paths(filters: &[String], debug: bool) -> Vec { + if !debug { + return Vec::new(); + } + + let trace_dir = PathBuf::from(TRACE_DIR); + if !trace_dir.exists() { + return Vec::new(); + } + + let Ok(entries) = fs::read_dir(&trace_dir) else { + return Vec::new(); + }; + + let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + entries.sort_by_key(|e| e.path()); + + let mut trace_paths = Vec::new(); + for entry in entries { + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "jsonl") { + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + + if !filters.is_empty() && !filters.iter().any(|f| file_name.contains(f)) { + continue; + } + + trace_paths.push(path); + } + } + + trace_paths +} + +/// Finds a trace file that matches the given snapshot path. +/// +/// Snapshot paths look like: `.../fixtures/checking/200_int_compare_transitive/Main.snap` +/// Trace files look like: `200_int_compare_transitive_Main.jsonl` +/// +/// We extract the test identifier (e.g., `200_int_compare_transitive`) from the snapshot's +/// parent directory and the module name from the file, then find a matching trace file. +pub fn match_checking_trace(snap_path: &Path, trace_paths: &[PathBuf]) -> Option { + let module_name = snap_path.file_stem()?.to_str()?; + let test_id = snap_path.parent()?.file_name()?.to_str()?; + + let expected_trace_name = format!("{}_{}.jsonl", test_id, module_name); + + trace_paths + .iter() + .find(|trace_path| { + trace_path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name == expected_trace_name) + }) + .cloned() +} diff --git a/compiler-scripts/src/test_runner/ui.rs b/compiler-scripts/src/test_runner/ui.rs new file mode 100644 index 00000000..6b8034ea --- /dev/null +++ b/compiler-scripts/src/test_runner/ui.rs @@ -0,0 +1,98 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::console::style; +use crate::snapshots::{print_diff, strip_frontmatter}; + +pub fn display_snapshot_diff( + snap: &Path, + snap_new: &Path, + short_path: &str, + trace_path: Option<&Path>, +) { + println!(); + println!("{} {}", style("UPDATED").yellow().bold(), style(short_path).cyan()); + + if let Some(trace) = trace_path { + println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); + } + + println!(); + + let old_content = fs::read_to_string(snap).unwrap_or_default(); + let new_content = fs::read_to_string(snap_new).unwrap_or_default(); + + let old_stripped = strip_frontmatter(&old_content); + let new_stripped = strip_frontmatter(&new_content); + + print_diff(old_stripped, new_stripped); +} + +pub fn display_new_snapshot(snap_new: &Path, short_path: &str, trace_path: Option<&Path>) { + println!("{} {}", style("CREATED").green().bold(), style(short_path).cyan()); + + if let Some(trace) = trace_path { + println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); + } + + println!(); + + let new_content = fs::read_to_string(snap_new).unwrap_or_default(); + for (i, line) in strip_frontmatter(&new_content).lines().enumerate() { + println!("{} {}", style(format!("{:3}", i + 1)).dim(), line); + } +} + +pub fn print_next_actions( + category_name: &str, + filters: &[String], + tests_passed: bool, + pending_count: usize, + trace_paths: &[PathBuf], + debug: bool, +) { + let filters_str = + if filters.is_empty() { String::new() } else { format!(" {}", filters.join(" ")) }; + + if tests_passed && pending_count == 0 { + println!("{}", style("No pending snapshots.").green()); + return; + } + + if pending_count > 0 { + println!("{}", style("-".repeat(60)).dim()); + println!(); + println!("{} pending snapshot{}", pending_count, if pending_count == 1 { "" } else { "s" }); + println!(); + println!(" If this is expected, run {}", style("cargo insta accept").cyan()); + println!(" If this is not expected, run {}", style("cargo insta reject").cyan()); + println!(); + } + + if !tests_passed { + println!("{}", style("-".repeat(60)).dim()); + println!(); + println!("{}", style("Tests failed, consider consulting trace files.").red()); + if !debug { + println!( + "Enable debug tracing for more information: {}", + style(format!("just t {} --debug{}", category_name, filters_str)).cyan() + ); + } + if !trace_paths.is_empty() { + let maximum_shown = 10; + for trace in trace_paths.iter().take(maximum_shown) { + println!("{} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); + } + if trace_paths.len() > maximum_shown { + let additional = trace_paths.len() - maximum_shown; + let additional = style(format!( + "An additional {additional} is stored in target/compiler-tracing" + )) + .dim(); + println!("{additional}"); + } + } + println!(); + } +} diff --git a/justfile b/justfile index 83a26d7a..bd3bf080 100644 --- a/justfile +++ b/justfile @@ -24,9 +24,13 @@ coverage-html: @integration *args="": cargo nextest run -p tests-integration "$@" --status-level=fail --final-status-level=fail --failure-output=final -[doc("Run checking tests with snapshot diffing. Use --help for options.")] +[doc("Run integration tests with snapshot diffing: checking|lowering|resolving|lsp")] +@t *args="": + cargo run -q -p compiler-scripts -- {{args}} + +[doc("Shorthand for 'just t checking'")] @tc *args="": - cargo run -q -p compiler-scripts --bin test-checking -- {{args}} + cargo run -q -p compiler-scripts -- checking {{args}} [doc("Apply clippy fixes and format")] fix: From fb58ba4ea3ee92066d1799cf029c46730da29aeb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 00:58:14 +0800 Subject: [PATCH 006/386] Improve lowering snapshot format with compact positions --- .../001_ado_statement_recursion/Main.snap | 12 ++-- .../lowering/002_class_equation/Main.snap | 44 ++++++------- .../lowering/003_data_equation/Main.snap | 20 +++--- .../lowering/004_derive_declaration/Main.snap | 4 +- .../lowering/005_do_statement/Main.snap | 36 +++++------ .../006_do_statement_recursion/Main.snap | 24 +++---- .../007_instance_declaration/Main.snap | 60 ++++++++--------- .../lowering/008_newtype_equation/Main.snap | 16 ++--- .../lowering/009_signature_equation/Main.snap | 12 ++-- .../lowering/010_value_equation/Main.snap | 46 ++++++------- .../lowering/011_case_after_let/Main.snap | 12 ++-- .../lowering/012_recursive_synonym/Main.snap | 40 ++++++------ .../lowering/013_ado_statement_let/Main.snap | 64 +++++++++---------- .../014_ado_statement_binder/Main.snap | 60 ++++++++--------- .../015_instance_constraints/Main.snap | 24 +++---- .../lowering/016_derive_constraints/Main.snap | 24 +++---- tests-integration/src/generated/basic.rs | 32 +++++----- 17 files changed, 266 insertions(+), 264 deletions(-) diff --git a/tests-integration/fixtures/lowering/001_ado_statement_recursion/Main.snap b/tests-integration/fixtures/lowering/001_ado_statement_recursion/Main.snap index 2fe8eb8b..3d761ef0 100644 --- a/tests-integration/fixtures/lowering/001_ado_statement_recursion/Main.snap +++ b/tests-integration/fixtures/lowering/001_ado_statement_recursion/Main.snap @@ -6,11 +6,11 @@ module Main Expressions: -x'notBinder@Some(Position { line: 4, character: 4 }) - resolves to binder Some(Position { line: 2, character: 10 }) -x'notBinder@Some(Position { line: 3, character: 21 }) - resolves to top-level name -pure@Some(Position { line: 3, character: 16 }) - resolves to top-level name +x'notBinder@4:4 + -> binder@2:10 +x'notBinder@3:21 + -> top-level +pure@3:16 + -> top-level Types: diff --git a/tests-integration/fixtures/lowering/002_class_equation/Main.snap b/tests-integration/fixtures/lowering/002_class_equation/Main.snap index aa353c73..cf180bec 100644 --- a/tests-integration/fixtures/lowering/002_class_equation/Main.snap +++ b/tests-integration/fixtures/lowering/002_class_equation/Main.snap @@ -9,25 +9,25 @@ Expressions: Types: -k@Some(Position { line: 2, character: 30 }) - resolves to forall Some(Position { line: 2, character: 22 }) -p@Some(Position { line: 4, character: 20 }) - resolves to forall Some(Position { line: 4, character: 17 }) -p@Some(Position { line: 4, character: 27 }) - resolves to forall Some(Position { line: 4, character: 17 }) -k@Some(Position { line: 2, character: 25 }) - resolves to forall Some(Position { line: 2, character: 22 }) -a@Some(Position { line: 7, character: 17 }) - resolves to forall Some(Position { line: 6, character: 17 }) -a@Some(Position { line: 6, character: 8 }) - resolves to forall Some(Position { line: 6, character: 17 }) -a@Some(Position { line: 4, character: 22 }) - resolves to forall Some(Position { line: 3, character: 12 }) -k@Some(Position { line: 3, character: 27 }) - resolves to forall Some(Position { line: 2, character: 22 }) -b@Some(Position { line: 4, character: 29 }) - resolves to forall Some(Position { line: 3, character: 21 }) -a@Some(Position { line: 7, character: 12 }) - resolves to forall Some(Position { line: 6, character: 17 }) -k@Some(Position { line: 3, character: 18 }) - resolves to forall Some(Position { line: 2, character: 22 }) +k@2:30 + -> forall@2:22 +p@4:20 + -> forall@4:17 +p@4:27 + -> forall@4:17 +k@2:25 + -> forall@2:22 +a@7:17 + -> forall@6:17 +a@6:8 + -> forall@6:17 +a@4:22 + -> forall@3:12 +k@3:27 + -> forall@2:22 +b@4:29 + -> forall@3:21 +a@7:12 + -> forall@6:17 +k@3:18 + -> forall@2:22 diff --git a/tests-integration/fixtures/lowering/003_data_equation/Main.snap b/tests-integration/fixtures/lowering/003_data_equation/Main.snap index 9595b3c3..60a5e7ad 100644 --- a/tests-integration/fixtures/lowering/003_data_equation/Main.snap +++ b/tests-integration/fixtures/lowering/003_data_equation/Main.snap @@ -9,13 +9,13 @@ Expressions: Types: -a@Some(Position { line: 4, character: 22 }) - resolves to forall Some(Position { line: 4, character: 11 }) -k@Some(Position { line: 6, character: 23 }) - resolves to forall Some(Position { line: 6, character: 20 }) -k@Some(Position { line: 7, character: 16 }) - resolves to forall Some(Position { line: 6, character: 20 }) -b@Some(Position { line: 4, character: 32 }) - resolves to forall Some(Position { line: 4, character: 13 }) -a@Some(Position { line: 2, character: 19 }) - resolves to forall Some(Position { line: 2, character: 10 }) +a@4:22 + -> forall@4:11 +k@6:23 + -> forall@6:20 +k@7:16 + -> forall@6:20 +b@4:32 + -> forall@4:13 +a@2:19 + -> forall@2:10 diff --git a/tests-integration/fixtures/lowering/004_derive_declaration/Main.snap b/tests-integration/fixtures/lowering/004_derive_declaration/Main.snap index a2b62a80..9f4e1340 100644 --- a/tests-integration/fixtures/lowering/004_derive_declaration/Main.snap +++ b/tests-integration/fixtures/lowering/004_derive_declaration/Main.snap @@ -9,7 +9,7 @@ Expressions: Types: -a@Some(Position { line: 2, character: 25 }) +a@2:25 introduces a constraint variable "a" -a@Some(Position { line: 3, character: 34 }) +a@3:34 introduces a constraint variable "a" diff --git a/tests-integration/fixtures/lowering/005_do_statement/Main.snap b/tests-integration/fixtures/lowering/005_do_statement/Main.snap index a1b98da7..e37385da 100644 --- a/tests-integration/fixtures/lowering/005_do_statement/Main.snap +++ b/tests-integration/fixtures/lowering/005_do_statement/Main.snap @@ -6,23 +6,23 @@ module Main Expressions: -x@Some(Position { line: 3, character: 8 }) - resolves to top-level name -pure@Some(Position { line: 5, character: 6 }) - resolves to top-level name -pure@Some(Position { line: 6, character: 12 }) - resolves to top-level name -x@Some(Position { line: 7, character: 8 }) - resolves to equation Some(Position { line: 4, character: 5 }) -action@Some(Position { line: 2, character: 9 }) - resolves to top-level name -y@Some(Position { line: 7, character: 12 }) - resolves to binder Some(Position { line: 4, character: 12 }) -z@Some(Position { line: 7, character: 16 }) - resolves to equation Some(Position { line: 6, character: 5 }) -z@Some(Position { line: 3, character: 12 }) - resolves to top-level name -y@Some(Position { line: 3, character: 10 }) - resolves to top-level name +x@3:8 + -> top-level +pure@5:6 + -> top-level +pure@6:12 + -> top-level +x@7:8 + -> equation@4:5 +action@2:9 + -> top-level +y@7:12 + -> binder@4:12 +z@7:16 + -> equation@6:5 +z@3:12 + -> top-level +y@3:10 + -> top-level Types: diff --git a/tests-integration/fixtures/lowering/006_do_statement_recursion/Main.snap b/tests-integration/fixtures/lowering/006_do_statement_recursion/Main.snap index ddc39c03..40c1a7bf 100644 --- a/tests-integration/fixtures/lowering/006_do_statement_recursion/Main.snap +++ b/tests-integration/fixtures/lowering/006_do_statement_recursion/Main.snap @@ -6,17 +6,17 @@ module Main Expressions: -y'equation@Some(Position { line: 3, character: 27 }) - resolves to equation Some(Position { line: 3, character: 5 }) -pure@Some(Position { line: 4, character: 33 }) - resolves to top-level name -pure@Some(Position { line: 4, character: 16 }) - resolves to top-level name -a'binder@Some(Position { line: 3, character: 38 }) - resolves to binder Some(Position { line: 3, character: 16 }) -x'notBinder@Some(Position { line: 5, character: 6 }) - resolves to binder Some(Position { line: 3, character: 47 }) -x'notBinder@Some(Position { line: 4, character: 21 }) - resolves to top-level name +y'equation@3:27 + -> equation@3:5 +pure@4:33 + -> top-level +pure@4:16 + -> top-level +a'binder@3:38 + -> binder@3:16 +x'notBinder@5:6 + -> binder@3:47 +x'notBinder@4:21 + -> top-level Types: diff --git a/tests-integration/fixtures/lowering/007_instance_declaration/Main.snap b/tests-integration/fixtures/lowering/007_instance_declaration/Main.snap index 82f9670d..7e491839 100644 --- a/tests-integration/fixtures/lowering/007_instance_declaration/Main.snap +++ b/tests-integration/fixtures/lowering/007_instance_declaration/Main.snap @@ -6,38 +6,38 @@ module Main Expressions: -eqMaybeImpl@Some(Position { line: 8, character: 6 }) - resolves to top-level name -eqIntImpl@Some(Position { line: 4, character: 6 }) - resolves to top-level name -b@Some(Position { line: 12, character: 11 }) - resolves to binder Some(Position { line: 12, character: 7 }) +eqMaybeImpl@8:6 + -> top-level +eqIntImpl@4:6 + -> top-level +b@12:11 + -> binder@12:7 Types: -a@Some(Position { line: 6, character: 11 }) - resolves to a constraint variable "a" - Some(Position { line: 6, character: 26 }) -b@Some(Position { line: 10, character: 21 }) - resolves to a constraint variable "b" - Some(Position { line: 10, character: 19 }) -a@Some(Position { line: 7, character: 24 }) - resolves to a constraint variable "a" - Some(Position { line: 6, character: 26 }) -b@Some(Position { line: 10, character: 19 }) +a@6:11 + -> constraint variable "a" + 6:26 +b@10:21 + -> constraint variable "b" + 10:19 +a@7:24 + -> constraint variable "a" + 6:26 +b@10:19 introduces a constraint variable "b" -a@Some(Position { line: 6, character: 26 }) +a@6:26 introduces a constraint variable "a" -a@Some(Position { line: 7, character: 13 }) - resolves to a constraint variable "a" - Some(Position { line: 6, character: 26 }) -b@Some(Position { line: 11, character: 22 }) - resolves to a constraint variable "b" - Some(Position { line: 10, character: 19 }) -b@Some(Position { line: 11, character: 29 }) - resolves to a constraint variable "b" - Some(Position { line: 10, character: 19 }) -p@Some(Position { line: 11, character: 20 }) - resolves to forall Some(Position { line: 11, character: 17 }) -p@Some(Position { line: 11, character: 27 }) - resolves to forall Some(Position { line: 11, character: 17 }) +a@7:13 + -> constraint variable "a" + 6:26 +b@11:22 + -> constraint variable "b" + 10:19 +b@11:29 + -> constraint variable "b" + 10:19 +p@11:20 + -> forall@11:17 +p@11:27 + -> forall@11:17 diff --git a/tests-integration/fixtures/lowering/008_newtype_equation/Main.snap b/tests-integration/fixtures/lowering/008_newtype_equation/Main.snap index cc8b5ded..8b7dbfc8 100644 --- a/tests-integration/fixtures/lowering/008_newtype_equation/Main.snap +++ b/tests-integration/fixtures/lowering/008_newtype_equation/Main.snap @@ -9,11 +9,11 @@ Expressions: Types: -a@Some(Position { line: 5, character: 29 }) - resolves to forall Some(Position { line: 5, character: 9 }) -k@Some(Position { line: 5, character: 15 }) - resolves to forall Some(Position { line: 4, character: 19 }) -k@Some(Position { line: 4, character: 22 }) - resolves to forall Some(Position { line: 4, character: 19 }) -a@Some(Position { line: 2, character: 17 }) - resolves to forall Some(Position { line: 2, character: 10 }) +a@5:29 + -> forall@5:9 +k@5:15 + -> forall@4:19 +k@4:22 + -> forall@4:19 +a@2:17 + -> forall@2:10 diff --git a/tests-integration/fixtures/lowering/009_signature_equation/Main.snap b/tests-integration/fixtures/lowering/009_signature_equation/Main.snap index 91402b58..2f4dbc65 100644 --- a/tests-integration/fixtures/lowering/009_signature_equation/Main.snap +++ b/tests-integration/fixtures/lowering/009_signature_equation/Main.snap @@ -9,9 +9,9 @@ Expressions: Types: -k@Some(Position { line: 3, character: 12 }) - resolves to forall Some(Position { line: 2, character: 16 }) -k@Some(Position { line: 2, character: 19 }) - resolves to forall Some(Position { line: 2, character: 16 }) -a@Some(Position { line: 3, character: 23 }) - resolves to forall Some(Position { line: 3, character: 6 }) +k@3:12 + -> forall@2:16 +k@2:19 + -> forall@2:16 +a@3:23 + -> forall@3:6 diff --git a/tests-integration/fixtures/lowering/010_value_equation/Main.snap b/tests-integration/fixtures/lowering/010_value_equation/Main.snap index 9007d5f3..be30321c 100644 --- a/tests-integration/fixtures/lowering/010_value_equation/Main.snap +++ b/tests-integration/fixtures/lowering/010_value_equation/Main.snap @@ -6,29 +6,29 @@ module Main Expressions: -a@Some(Position { line: 3, character: 15 }) - resolves to binder Some(Position { line: 3, character: 4 }) -z@Some(Position { line: 10, character: 4 }) - resolves to signature Some(Position { line: 7, character: 5 }) - resolves to equation Some(Position { line: 8, character: 12 }) -add@Some(Position { line: 14, character: 6 }) - resolves to top-level name -a@Some(Position { line: 9, character: 7 }) - resolves to binder Some(Position { line: 6, character: 5 }) +a@3:15 + -> binder@3:4 +z@10:4 + -> signature@7:5 + -> equation@8:12 +add@14:6 + -> top-level +a@9:7 + -> binder@6:5 Types: -a@Some(Position { line: 3, character: 19 }) - resolves to forall Some(Position { line: 2, character: 12 }) -a@Some(Position { line: 2, character: 20 }) - resolves to forall Some(Position { line: 2, character: 12 }) -a@Some(Position { line: 5, character: 20 }) - resolves to forall Some(Position { line: 5, character: 15 }) -a@Some(Position { line: 2, character: 15 }) - resolves to forall Some(Position { line: 2, character: 12 }) -a@Some(Position { line: 5, character: 30 }) - resolves to forall Some(Position { line: 5, character: 15 }) -a@Some(Position { line: 3, character: 8 }) - resolves to forall Some(Position { line: 2, character: 12 }) -b@Some(Position { line: 5, character: 25 }) - resolves to forall Some(Position { line: 5, character: 17 }) +a@3:19 + -> forall@2:12 +a@2:20 + -> forall@2:12 +a@5:20 + -> forall@5:15 +a@2:15 + -> forall@2:12 +a@5:30 + -> forall@5:15 +a@3:8 + -> forall@2:12 +b@5:25 + -> forall@5:17 diff --git a/tests-integration/fixtures/lowering/011_case_after_let/Main.snap b/tests-integration/fixtures/lowering/011_case_after_let/Main.snap index 653f19ba..2b2312aa 100644 --- a/tests-integration/fixtures/lowering/011_case_after_let/Main.snap +++ b/tests-integration/fixtures/lowering/011_case_after_let/Main.snap @@ -6,12 +6,12 @@ module Main Expressions: -b@Some(Position { line: 12, character: 17 }) - resolves to binder Some(Position { line: 12, character: 12 }) -a@Some(Position { line: 9, character: 17 }) - resolves to binder Some(Position { line: 9, character: 12 }) +b@12:17 + -> binder@12:12 +a@9:17 + -> binder@9:12 Types: -a@Some(Position { line: 2, character: 19 }) - resolves to forall Some(Position { line: 2, character: 10 }) +a@2:19 + -> forall@2:10 diff --git a/tests-integration/fixtures/lowering/012_recursive_synonym/Main.snap b/tests-integration/fixtures/lowering/012_recursive_synonym/Main.snap index 2e3748de..9a327928 100644 --- a/tests-integration/fixtures/lowering/012_recursive_synonym/Main.snap +++ b/tests-integration/fixtures/lowering/012_recursive_synonym/Main.snap @@ -9,23 +9,23 @@ Expressions: Types: -a@Some(Position { line: 28, character: 26 }) - resolves to forall Some(Position { line: 28, character: 13 }) -a@Some(Position { line: 25, character: 37 }) - resolves to forall Some(Position { line: 25, character: 10 }) -a@Some(Position { line: 20, character: 38 }) - resolves to forall Some(Position { line: 20, character: 14 }) -a@Some(Position { line: 30, character: 22 }) - resolves to nothing -a@Some(Position { line: 19, character: 27 }) - resolves to forall Some(Position { line: 19, character: 13 }) -a@Some(Position { line: 30, character: 33 }) - resolves to nothing -a@Some(Position { line: 27, character: 22 }) - resolves to nothing -a@Some(Position { line: 27, character: 33 }) - resolves to nothing -k@Some(Position { line: 23, character: 38 }) - resolves to forall Some(Position { line: 23, character: 35 }) -a@Some(Position { line: 31, character: 26 }) - resolves to forall Some(Position { line: 31, character: 13 }) +a@28:26 + -> forall@28:13 +a@25:37 + -> forall@25:10 +a@20:38 + -> forall@20:14 +a@30:22 + -> nothing +a@19:27 + -> forall@19:13 +a@30:33 + -> nothing +a@27:22 + -> nothing +a@27:33 + -> nothing +k@23:38 + -> forall@23:35 +a@31:26 + -> forall@31:13 diff --git a/tests-integration/fixtures/lowering/013_ado_statement_let/Main.snap b/tests-integration/fixtures/lowering/013_ado_statement_let/Main.snap index 5b9852cf..a5e925f8 100644 --- a/tests-integration/fixtures/lowering/013_ado_statement_let/Main.snap +++ b/tests-integration/fixtures/lowering/013_ado_statement_let/Main.snap @@ -6,38 +6,38 @@ module Main Expressions: -pure@Some(Position { line: 11, character: 6 }) - resolves to top-level name -{ x, y, z }@Some(Position { line: 12, character: 4 }) - resolves to binder Some(Position { line: 8, character: 10 }) -{ x, y, z }@Some(Position { line: 12, character: 4 }) - resolves to equation Some(Position { line: 10, character: 5 }) -{ x, y, z }@Some(Position { line: 12, character: 4 }) - resolves to binder Some(Position { line: 10, character: 15 }) -{ x }@Some(Position { line: 10, character: 9 }) - resolves to binder Some(Position { line: 8, character: 10 }) -pure@Some(Position { line: 9, character: 6 }) - resolves to top-level name +pure@11:6 + -> top-level +{ x, y, z }@12:4 + -> binder@8:10 +{ x, y, z }@12:4 + -> equation@10:5 +{ x, y, z }@12:4 + -> binder@10:15 +{ x }@10:9 + -> binder@8:10 +pure@9:6 + -> top-level Types: -b@Some(Position { line: 5, character: 39 }) - resolves to forall Some(Position { line: 5, character: 30 }) -a@Some(Position { line: 6, character: 61 }) - resolves to forall Some(Position { line: 6, character: 30 }) -b@Some(Position { line: 6, character: 73 }) - resolves to forall Some(Position { line: 6, character: 32 }) -b@Some(Position { line: 6, character: 48 }) - resolves to forall Some(Position { line: 6, character: 32 }) -a@Some(Position { line: 5, character: 35 }) - resolves to forall Some(Position { line: 5, character: 28 }) -a@Some(Position { line: 6, character: 44 }) - resolves to forall Some(Position { line: 6, character: 30 }) -a@Some(Position { line: 5, character: 52 }) - resolves to forall Some(Position { line: 5, character: 28 }) -b@Some(Position { line: 5, character: 64 }) - resolves to forall Some(Position { line: 5, character: 30 }) -a@Some(Position { line: 4, character: 32 }) - resolves to forall Some(Position { line: 4, character: 29 }) -a@Some(Position { line: 4, character: 44 }) - resolves to forall Some(Position { line: 4, character: 29 }) +b@5:39 + -> forall@5:30 +a@6:61 + -> forall@6:30 +b@6:73 + -> forall@6:32 +b@6:48 + -> forall@6:32 +a@5:35 + -> forall@5:28 +a@6:44 + -> forall@6:30 +a@5:52 + -> forall@5:28 +b@5:64 + -> forall@5:30 +a@4:32 + -> forall@4:29 +a@4:44 + -> forall@4:29 diff --git a/tests-integration/fixtures/lowering/014_ado_statement_binder/Main.snap b/tests-integration/fixtures/lowering/014_ado_statement_binder/Main.snap index e7d2ca40..61f407e3 100644 --- a/tests-integration/fixtures/lowering/014_ado_statement_binder/Main.snap +++ b/tests-integration/fixtures/lowering/014_ado_statement_binder/Main.snap @@ -6,36 +6,36 @@ module Main Expressions: -pure@Some(Position { line: 10, character: 6 }) - resolves to top-level name -{ x, y }@Some(Position { line: 11, character: 4 }) - resolves to binder Some(Position { line: 8, character: 10 }) -{ x, y }@Some(Position { line: 11, character: 4 }) - resolves to binder Some(Position { line: 9, character: 14 }) -x@Some(Position { line: 10, character: 11 }) - resolves to binder Some(Position { line: 8, character: 10 }) -pure@Some(Position { line: 9, character: 6 }) - resolves to top-level name +pure@10:6 + -> top-level +{ x, y }@11:4 + -> binder@8:10 +{ x, y }@11:4 + -> binder@9:14 +x@10:11 + -> binder@8:10 +pure@9:6 + -> top-level Types: -b@Some(Position { line: 5, character: 39 }) - resolves to forall Some(Position { line: 5, character: 30 }) -a@Some(Position { line: 6, character: 61 }) - resolves to forall Some(Position { line: 6, character: 30 }) -b@Some(Position { line: 6, character: 73 }) - resolves to forall Some(Position { line: 6, character: 32 }) -b@Some(Position { line: 6, character: 48 }) - resolves to forall Some(Position { line: 6, character: 32 }) -a@Some(Position { line: 5, character: 35 }) - resolves to forall Some(Position { line: 5, character: 28 }) -a@Some(Position { line: 6, character: 44 }) - resolves to forall Some(Position { line: 6, character: 30 }) -a@Some(Position { line: 5, character: 52 }) - resolves to forall Some(Position { line: 5, character: 28 }) -b@Some(Position { line: 5, character: 64 }) - resolves to forall Some(Position { line: 5, character: 30 }) -a@Some(Position { line: 4, character: 32 }) - resolves to forall Some(Position { line: 4, character: 29 }) -a@Some(Position { line: 4, character: 44 }) - resolves to forall Some(Position { line: 4, character: 29 }) +b@5:39 + -> forall@5:30 +a@6:61 + -> forall@6:30 +b@6:73 + -> forall@6:32 +b@6:48 + -> forall@6:32 +a@5:35 + -> forall@5:28 +a@6:44 + -> forall@6:30 +a@5:52 + -> forall@5:28 +b@5:64 + -> forall@5:30 +a@4:32 + -> forall@4:29 +a@4:44 + -> forall@4:29 diff --git a/tests-integration/fixtures/lowering/015_instance_constraints/Main.snap b/tests-integration/fixtures/lowering/015_instance_constraints/Main.snap index 75a5dc1a..8d689d77 100644 --- a/tests-integration/fixtures/lowering/015_instance_constraints/Main.snap +++ b/tests-integration/fixtures/lowering/015_instance_constraints/Main.snap @@ -9,18 +9,18 @@ Expressions: Types: -n@Some(Position { line: 6, character: 22 }) - resolves to a constraint variable "n" - Some(Position { line: 6, character: 48 }) -n@Some(Position { line: 6, character: 48 }) +n@6:22 + -> constraint variable "n" + 6:48 +n@6:48 introduces a constraint variable "n" -o@Some(Position { line: 6, character: 36 }) - resolves to a constraint variable "o" - Some(Position { line: 6, character: 50 }) -m@Some(Position { line: 6, character: 24 }) +o@6:36 + -> constraint variable "o" + 6:50 +m@6:24 introduces a constraint variable "m" -o@Some(Position { line: 6, character: 50 }) +o@6:50 introduces a constraint variable "o" -m@Some(Position { line: 6, character: 34 }) - resolves to a constraint variable "m" - Some(Position { line: 6, character: 24 }) +m@6:34 + -> constraint variable "m" + 6:24 diff --git a/tests-integration/fixtures/lowering/016_derive_constraints/Main.snap b/tests-integration/fixtures/lowering/016_derive_constraints/Main.snap index 82768517..eaac4312 100644 --- a/tests-integration/fixtures/lowering/016_derive_constraints/Main.snap +++ b/tests-integration/fixtures/lowering/016_derive_constraints/Main.snap @@ -9,18 +9,18 @@ Expressions: Types: -m@Some(Position { line: 6, character: 31 }) +m@6:31 introduces a constraint variable "m" -o@Some(Position { line: 6, character: 57 }) +o@6:57 introduces a constraint variable "o" -m@Some(Position { line: 6, character: 41 }) - resolves to a constraint variable "m" - Some(Position { line: 6, character: 31 }) -n@Some(Position { line: 6, character: 29 }) - resolves to a constraint variable "n" - Some(Position { line: 6, character: 55 }) -n@Some(Position { line: 6, character: 55 }) +m@6:41 + -> constraint variable "m" + 6:31 +n@6:29 + -> constraint variable "n" + 6:55 +n@6:55 introduces a constraint variable "n" -o@Some(Position { line: 6, character: 43 }) - resolves to a constraint variable "o" - Some(Position { line: 6, character: 57 }) +o@6:43 + -> constraint variable "o" + 6:57 diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index f8a38cb0..f846b7d1 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -161,7 +161,8 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { macro_rules! pos { ($id:expr) => {{ let cst = stabilized.ast_ptr($id).unwrap(); - locate::offset_to_position(&content, cst.syntax_node_ptr().text_range().start()) + let p = locate::offset_to_position(&content, cst.syntax_node_ptr().text_range().start()).unwrap(); + format!("{}:{}", p.line, p.character) }}; } @@ -174,10 +175,10 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { let node = cst.syntax_node_ptr().to_node(module.syntax()); let text = node.text().to_string(); - writeln!(buffer, "{}@{:?}", text.trim(), pos!(type_id)).unwrap(); + writeln!(buffer, "{}@{}", text.trim(), pos!(type_id)).unwrap(); match resolution { Some(TypeVariableResolution::Forall(id)) => { - writeln!(buffer, " resolves to forall {:?}", pos!(*id)).unwrap(); + writeln!(buffer, " -> forall@{}", pos!(*id)).unwrap(); } Some(TypeVariableResolution::Implicit(ImplicitTypeVariable { binding, node, id })) => { let GraphNode::Implicit { bindings, .. } = &graph[*node] else { @@ -189,14 +190,14 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { if *binding { writeln!(buffer, " introduces a constraint variable {name:?}").unwrap(); } else { - writeln!(buffer, " resolves to a constraint variable {name:?}").unwrap(); + writeln!(buffer, " -> constraint variable {name:?}").unwrap(); for &tid in type_ids { - writeln!(buffer, " {:?}", pos!(tid)).unwrap(); + writeln!(buffer, " {}", pos!(tid)).unwrap(); } } } None => { - writeln!(buffer, " resolves to nothing").unwrap(); + writeln!(buffer, " -> nothing").unwrap(); } } } @@ -216,38 +217,39 @@ fn report_on_term( let cst = stabilized.ast_ptr(expression_id).unwrap(); let node = cst.syntax_node_ptr().to_node(module.syntax()); let text = node.text().to_string(); - let position = locate::offset_to_position(content, node.text_range().start()); + let position = locate::offset_to_position(content, node.text_range().start()).unwrap(); - writeln!(buffer, "{}@{:?}", text.trim(), position).unwrap(); + writeln!(buffer, "{}@{}:{}", text.trim(), position.line, position.character).unwrap(); macro_rules! pos { ($id:expr) => {{ let cst = stabilized.ast_ptr($id).unwrap(); - locate::offset_to_position(content, cst.syntax_node_ptr().text_range().start()) + let p = locate::offset_to_position(content, cst.syntax_node_ptr().text_range().start()).unwrap(); + format!("{}:{}", p.line, p.character) }}; } match resolution { Some(TermVariableResolution::Binder(id)) => { - writeln!(buffer, " resolves to binder {:?}", pos!(*id)).unwrap(); + writeln!(buffer, " -> binder@{}", pos!(*id)).unwrap(); } Some(TermVariableResolution::Let(let_binding_id)) => { let let_binding = info.get_let_binding_group(*let_binding_id); if let Some(sig) = let_binding.signature { - writeln!(buffer, " resolves to signature {:?}", pos!(sig)).unwrap(); + writeln!(buffer, " -> signature@{}", pos!(sig)).unwrap(); } for eq in let_binding.equations.iter() { - writeln!(buffer, " resolves to equation {:?}", pos!(*eq)).unwrap(); + writeln!(buffer, " -> equation@{}", pos!(*eq)).unwrap(); } } Some(TermVariableResolution::RecordPun(id)) => { - writeln!(buffer, " resolves to record pun {:?}", pos!(*id)).unwrap(); + writeln!(buffer, " -> record pun@{}", pos!(*id)).unwrap(); } Some(TermVariableResolution::Reference(..)) => { - writeln!(buffer, " resolves to top-level name").unwrap(); + writeln!(buffer, " -> top-level").unwrap(); } None => { - writeln!(buffer, " resolves to nothing").unwrap(); + writeln!(buffer, " -> nothing").unwrap(); } } } From 7f64a392db17ccb8675297b67851f433e661c6f3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 01:03:14 +0800 Subject: [PATCH 007/386] Update AGENTS.md --- AGENTS.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 100e299b..325212f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,18 @@ ## Commands ```bash -cargo check -p --tests # Type check a crate (always specify -p) -cargo nextest run -p # Run all tests in a crate +cargo check -p --tests # Type check a crate (always specify -p) +just t checking [filters...] # Type checker integration tests +just t lowering [filters...] # Lowering integration tests +just t resolving [filters...] # Resolver integration tests +just t lsp [filters...] # LSP integration tests +just fix # Apply clippy fixes and format +``` + +For unit tests in compiler-core (not tests-integration which requires the test runner shim): +```bash +cargo nextest run -p # Run all tests in a crate cargo nextest run -p # Run single test -just tc # Type checker integration tests -just tc 101 # Run specific test (by filter) -just fix # Apply clippy fixes and format ``` ## Architecture From 875d51bb440d1c80296e4ed898e35484032f5a75 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 01:05:16 +0800 Subject: [PATCH 008/386] Update type-checker-tests --- .claude/skills/type-checker-tests/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index b7b82086..ea5ea199 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -15,9 +15,9 @@ Use this skill when adding new type checker functions or expanding behavior. | Action | Command | |--------|---------| | Find next test number | `ls tests-integration/fixtures/checking/ \| tail -5` | -| Run a test or multiple tests | `just t checking NNN` or `just tc 101 102` | +| Run a test or multiple tests | `just t checking NNN` or `just t c 101 102` | | Run with tracing enabled | `just t checking --debug NNN` | -| Run all checking tests | `just t checking` or `just tc` | +| Run all checking tests | `just t checking` or `just t c` | | Accept all pending snapshots | `cargo insta accept` | Use `just t checking --help` for all options. From 7d08d417fae4eb2d3797cbc9a08416a1f9355039 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 01:17:17 +0800 Subject: [PATCH 009/386] Add diagnostics crate to AGENTS.md --- compiler-core/AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler-core/AGENTS.md b/compiler-core/AGENTS.md index 3011ef90..9517970f 100644 --- a/compiler-core/AGENTS.md +++ b/compiler-core/AGENTS.md @@ -14,6 +14,7 @@ transparency (editor introspection) and compatibility with query-based increment - **resolving**: name-indexed interface for module items - **stabilizing**: assigns stable IDs to source ranges - **checking**: type checking and elaboration +- **diagnostics**: error collection and rendering for LSP and tests ### Infrastructure From e3852cbaa981c0343f8e2872556dcd6ad3fa6eb0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 01:37:45 +0800 Subject: [PATCH 010/386] Discard annotation positions for diagnostics --- Cargo.lock | 2 + compiler-bin/Cargo.toml | 2 + compiler-bin/src/lsp/event.rs | 269 ++++-------------- compiler-core/diagnostics/src/context.rs | 22 +- compiler-core/diagnostics/src/convert.rs | 3 +- compiler-core/diagnostics/src/render.rs | 32 ++- .../032_recursive_synonym_expansion/Main.snap | 18 +- .../078_inspect_arity_invalid/Main.snap | 6 +- .../080_let_recursive_errors/Main.snap | 10 +- .../checking/084_instance_eq/Main.snap | 2 +- .../checking/089_no_instance_found/Main.snap | 2 +- .../092_ambiguous_constraint/Main.snap | 4 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../111_int_add_invalid_no_instance/Main.snap | 2 +- .../112_int_mul_invalid_no_instance/Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../checking/115_empty_do_block/Main.snap | 4 +- .../checking/116_empty_ado_block/Main.snap | 4 +- .../checking/117_do_ado_constrained/Main.snap | 2 +- .../checking/127_derive_eq_simple/Main.snap | 2 +- .../131_derive_eq_missing_instance/Main.snap | 2 +- .../132_derive_eq_1_higher_kinded/Main.snap | 2 +- .../checking/133_derive_eq_partial/Main.snap | 2 +- .../checking/134_derive_ord_simple/Main.snap | 2 +- .../135_derive_ord_1_higher_kinded/Main.snap | 4 +- .../136_derive_nested_higher_kinded/Main.snap | 8 +- .../142_derive_newtype_not_newtype/Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../153_derive_contravariant_error/Main.snap | 4 +- .../155_derive_profunctor_error/Main.snap | 6 +- .../Main.snap | 4 +- .../Main.snap | 8 +- .../Main.snap | 2 +- .../Main.snap | 4 +- .../Main.snap | 4 +- .../checking/167_derive_eq_1/Main.snap | 4 +- .../checking/168_derive_ord_1/Main.snap | 6 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../checking/190_coercible_nominal/Main.snap | 2 +- .../191_coercible_newtype_hidden/Main.snap | 8 +- .../Main.snap | 4 +- .../Main.snap | 2 +- .../202_int_compare_invalid/Main.snap | 14 +- .../checking/205_builtin_warn/Main.snap | 16 +- .../checking/206_builtin_fail/Main.snap | 4 +- 54 files changed, 193 insertions(+), 339 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dd47588..b031ff53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,9 +1404,11 @@ dependencies = [ "async-lsp", "checking", "clap", + "diagnostics", "files", "globset", "indexing", + "itertools 0.14.0", "lowering", "parking_lot", "path-absolutize", diff --git a/compiler-bin/Cargo.toml b/compiler-bin/Cargo.toml index 9e32f00a..9dae6282 100644 --- a/compiler-bin/Cargo.toml +++ b/compiler-bin/Cargo.toml @@ -17,12 +17,14 @@ no-tracing = ["checking/no-tracing"] [dependencies] checking = { version = "0.1.0", path = "../compiler-core/checking" } +diagnostics = { version = "0.1.0", path = "../compiler-core/diagnostics" } analyzer = { version = "0.1.0", path = "../compiler-lsp/analyzer" } async-lsp = "0.2.2" clap = { version = "4.5.53", features = ["derive"] } files = { version = "0.1.0", path = "../compiler-core/files" } globset = "0.4.18" indexing = { version = "0.1.0", path = "../compiler-core/indexing" } +itertools = "0.14.0" lowering = { version = "0.1.0", path = "../compiler-core/lowering" } parking_lot = "0.12.5" path-absolutize = "3.1.1" diff --git a/compiler-bin/src/lsp/event.rs b/compiler-bin/src/lsp/event.rs index 0cec2785..eea66bb9 100644 --- a/compiler-bin/src/lsp/event.rs +++ b/compiler-bin/src/lsp/event.rs @@ -1,27 +1,14 @@ use analyzer::{common, locate}; use async_lsp::LanguageClient; use async_lsp::lsp_types::*; +use diagnostics::{DiagnosticsContext, ToDiagnostics}; use files::FileId; -use indexing::{IndexedModule, TypeItemKind}; -use lowering::{LoweringError, RecursiveGroup}; -use resolving::{ResolvedModule, ResolvingError}; -use rowan::ast::AstNode; -use stabilizing::StabilizedModule; -use syntax::SyntaxNode; +use itertools::Itertools; +use rowan::TextSize; use crate::lsp::error::LspError; use crate::lsp::{State, StateSnapshot}; -struct DiagnosticsContext<'a> { - uri: &'a Url, - content: &'a str, - root: &'a SyntaxNode, - stabilized: &'a StabilizedModule, - indexed: &'a IndexedModule, - resolved: &'a ResolvedModule, - lowered: &'a lowering::LoweredModule, -} - pub fn emit_collect_diagnostics(state: &mut State, uri: Url) -> Result<(), LspError> { let files = state.files.read(); let uri = uri.as_str(); @@ -56,231 +43,77 @@ fn collect_diagnostics_core( let indexed = snapshot.engine.indexed(id)?; let resolved = snapshot.engine.resolved(id)?; let lowered = snapshot.engine.lowered(id)?; + let checked = snapshot.engine.checked(id)?; let uri = { let files = snapshot.files.read(); common::file_uri(&snapshot.engine, &files, id)? }; - let context = DiagnosticsContext { - uri: &uri, - content: &content, - root: &root, - stabilized: &stabilized, - indexed: &indexed, - resolved: &resolved, - lowered: &lowered, - }; - - let mut diagnostics = vec![]; - diagnostics.extend(resolved_diagnostics(&context)); - diagnostics.extend(lowered_diagnostics(&context)); - - snapshot.client.publish_diagnostics(PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - })?; - - Ok(()) -} - -fn lowered_diagnostics<'a>( - context: &'a DiagnosticsContext<'a>, -) -> impl Iterator + 'a { - context.lowered.errors.iter().filter_map(|error| lowered_error(context, error)) -} - -fn lowered_error(context: &DiagnosticsContext<'_>, error: &LoweringError) -> Option { - match error { - LoweringError::NotInScope(not_in_scope) => { - let (ptr, name) = match not_in_scope { - lowering::NotInScope::ExprConstructor { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::ExprVariable { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::ExprOperatorName { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::TypeConstructor { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::TypeVariable { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::TypeOperatorName { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::NegateFn { id } => { - (context.stabilized.syntax_ptr(*id)?, Some("negate")) - } - lowering::NotInScope::DoFn { kind, id } => ( - context.stabilized.syntax_ptr(*id)?, - match kind { - lowering::DoFn::Bind => Some("bind"), - lowering::DoFn::Discard => Some("discard"), - }, - ), - lowering::NotInScope::AdoFn { kind, id } => ( - context.stabilized.syntax_ptr(*id)?, - match kind { - lowering::AdoFn::Map => Some("map"), - lowering::AdoFn::Apply => Some("apply"), - lowering::AdoFn::Pure => Some("pure"), - }, - ), - lowering::NotInScope::TermOperator { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - lowering::NotInScope::TypeOperator { id } => { - (context.stabilized.syntax_ptr(*id)?, None) - } - }; - - let message = if let Some(name) = name { - format!("'{name}' is not in scope") - } else { - let range = ptr.to_node(context.root).text_range(); - let name = context.content[range].trim(); - format!("'{name}' is not in scope") - }; - - let range = locate::syntax_range(context.content, context.root, &ptr)?; - - Some(Diagnostic { - range, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(NumberOrString::String("NotInScope".to_string())), - code_description: None, - source: Some("analyzer/lowering".to_string()), - message: message.to_string(), - related_information: None, - tags: None, - data: None, - }) - } - - LoweringError::RecursiveSynonym(RecursiveGroup { group }) => { - let equations = group.iter().filter_map(|id| { - if let TypeItemKind::Synonym { equation, .. } = context.indexed.items[*id].kind { - equation - } else { - None - } - }); - - let locations = equations.filter_map(|equation| { - let syntax_ptr = context.stabilized.syntax_ptr(equation)?; - locate::syntax_range(context.content, context.root, &syntax_ptr) - }); - - let locations: Vec<_> = locations.collect(); - let [range, associated @ ..] = &locations[..] else { return None }; - - let related_information = associated.iter().map(|&range| { - let uri = context.uri.clone(); - let location = Location { uri, range }; - DiagnosticRelatedInformation { - location, - message: "Includes this type synonym".to_string(), - } - }); - - let related_information = related_information.collect(); + let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &checked); - Some(Diagnostic { - range: *range, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(NumberOrString::String("RecursiveSynonym".to_string())), - code_description: None, - source: Some("analyzer/lowering".to_string()), - message: "Invalid type synonym cycle".to_string(), - related_information: Some(related_information), - tags: None, - data: None, - }) - } + let mut all_diagnostics = vec![]; - _ => None, + for error in &lowered.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); } -} - -fn resolved_diagnostics<'a>( - context: &'a DiagnosticsContext<'a>, -) -> impl Iterator + 'a { - context.resolved.errors.iter().filter_map(|error| resolved_error(context, error)) -} -fn resolved_error(context: &DiagnosticsContext<'_>, error: &ResolvingError) -> Option { - let source = Some("analyzer/resolving".to_string()); - match error { - ResolvingError::TermImportConflict { .. } => None, - - ResolvingError::TypeImportConflict { .. } => None, - - ResolvingError::TermExportConflict { .. } => None, - - ResolvingError::TypeExportConflict { .. } => None, - - ResolvingError::ExistingTerm { .. } => None, - - ResolvingError::ExistingType { .. } => None, + for error in &resolved.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } - ResolvingError::InvalidImportStatement { id } => { - let ptr = context.stabilized.ast_ptr(*id)?; + for error in &checked.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } - let message = { - let cst = ptr.to_node(context.root); + let to_position = |offset: u32| locate::offset_to_position(&content, TextSize::from(offset)); - let name = cst.module_name().map(|cst| { - let range = cst.syntax().text_range(); - context.content[range].trim() - }); + let diagnostics = all_diagnostics + .iter() + .filter_map(|diagnostic| { + let start = to_position(diagnostic.primary.start)?; + let end = to_position(diagnostic.primary.end)?; + let range = Range { start, end }; - let name = name.unwrap_or(""); - format!("Cannot import module '{name}'") + let severity = match diagnostic.severity { + diagnostics::Severity::Error => DiagnosticSeverity::ERROR, + diagnostics::Severity::Warning => DiagnosticSeverity::WARNING, }; - let ptr = ptr.syntax_node_ptr(); - let range = locate::syntax_range(context.content, context.root, &ptr)?; + let related_information = diagnostic.related.iter().filter_map(|related| { + let start = to_position(related.span.start)?; + let end = to_position(related.span.end)?; + Some(DiagnosticRelatedInformation { + location: Location { uri: uri.clone(), range: Range { start, end } }, + message: related.message.clone(), + }) + }); + + let related_information = related_information.collect_vec(); Some(Diagnostic { range, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(NumberOrString::String("InvalidImportStatement".to_string())), + severity: Some(severity), + code: Some(NumberOrString::String(diagnostic.code.to_string())), code_description: None, - source, - message, - related_information: None, + source: Some(format!("analyzer/{}", diagnostic.source)), + message: diagnostic.message.clone(), + related_information: if related_information.is_empty() { + None + } else { + Some(related_information) + }, tags: None, data: None, }) - } - - ResolvingError::InvalidImportItem { id } => { - let ptr = context.stabilized.syntax_ptr(*id)?; - - let message = { - let range = ptr.to_node(context.root).text_range(); - let name = context.content[range].trim(); - format!("Cannot import item '{name}'") - }; + }) + .collect(); - let range = locate::syntax_range(context.content, context.root, &ptr)?; + snapshot.client.publish_diagnostics(PublishDiagnosticsParams { + uri, + diagnostics, + version: None, + })?; - Some(Diagnostic { - range, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(NumberOrString::String("InvalidImportItem".to_string())), - code_description: None, - source, - message, - related_information: None, - tags: None, - data: None, - }) - } - } + Ok(()) } diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index a34ae3ec..8eb3172e 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -3,7 +3,7 @@ use checking::error::ErrorStep; use indexing::IndexedModule; use rowan::ast::{AstNode, AstPtr}; use stabilizing::StabilizedModule; -use syntax::{SyntaxNode, SyntaxNodePtr}; +use syntax::{SyntaxKind, SyntaxNode, SyntaxNodePtr}; use crate::Span; @@ -27,16 +27,28 @@ impl<'a> DiagnosticsContext<'a> { } pub fn span_from_syntax_ptr(&self, ptr: &SyntaxNodePtr) -> Option { - let range = ptr.to_node(self.root).text_range(); - Some(Span::new(range.start().into(), range.end().into())) + let node = ptr.try_to_node(self.root)?; + self.span_from_syntax_node(&node) } pub fn span_from_ast_ptr>( &self, ptr: &AstPtr, ) -> Option { - let ptr = ptr.syntax_node_ptr(); - self.span_from_syntax_ptr(&ptr) + let node = ptr.try_to_node(self.root)?; + self.span_from_syntax_node(node.syntax()) + } + + fn span_from_syntax_node(&self, node: &SyntaxNode) -> Option { + let mut children = node.children_with_tokens().peekable(); + + children.next_if(|child| matches!(child.kind(), SyntaxKind::Annotation)); + + let start = children.peek().map(|child| child.text_range()); + let end = children.last().map(|child| child.text_range()); + + let range = start.zip(end).map(|(start, end)| start.cover(end))?; + Some(Span::new(range.start().into(), range.end().into())) } pub fn text_of(&self, span: Span) -> &'a str { diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index c1961867..3bf0ed1f 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -141,8 +141,7 @@ impl ToDiagnostics for ResolvingError { format!("Cannot import module '{name}'") }; - let ptr = ptr.syntax_node_ptr(); - let Some(span) = ctx.span_from_syntax_ptr(&ptr) else { return vec![] }; + let Some(span) = ctx.span_from_ast_ptr(&ptr) else { return vec![] }; vec![Diagnostic::error("InvalidImportStatement", message, span, "resolving")] } diff --git a/compiler-core/diagnostics/src/render.rs b/compiler-core/diagnostics/src/render.rs index f0509856..5ebbde29 100644 --- a/compiler-core/diagnostics/src/render.rs +++ b/compiler-core/diagnostics/src/render.rs @@ -1,4 +1,9 @@ use itertools::Itertools; +use line_index::{LineCol, LineIndex}; +use lsp_types::{ + DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString, Position, Range, +}; +use rowan::TextSize; use crate::{Diagnostic, Severity}; @@ -27,26 +32,27 @@ pub fn format_text(diagnostics: &[Diagnostic]) -> String { output } +fn offset_to_position(line_index: &LineIndex, content: &str, offset: TextSize) -> Option { + let LineCol { line, col } = line_index.line_col(offset); + + let line_text_range = line_index.line(line)?; + let line_content = &content[line_text_range]; + + let until_col = &line_content[..col as usize]; + let character = until_col.chars().count() as u32; + + Some(Position { line, character }) +} + pub fn to_lsp_diagnostic( diagnostic: &Diagnostic, content: &str, uri: &lsp_types::Url, ) -> Option { - use line_index::{LineCol, LineIndex}; - use lsp_types::{ - DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString, Position, Range, - }; - let line_index = LineIndex::new(content); - let to_position = |offset: u32| -> Option { - let LineCol { line, col } = line_index.line_col(offset.into()); - let line_range = line_index.line(line)?; - let line_content = &content[line_range]; - let until_col = &line_content[..col as usize]; - let character = until_col.chars().count() as u32; - Some(Position { line, character }) - }; + let to_position = + |offset: u32| offset_to_position(&line_index, content, TextSize::from(offset)); let start = to_position(diagnostic.primary.start)?; let end = to_position(diagnostic.primary.end)?; diff --git a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap index 83a5a8a8..752befd7 100644 --- a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap @@ -37,12 +37,12 @@ Valid = Int Diagnostics -error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 52..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 81..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 110..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion diff --git a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap index 20164259..3d2115ae 100644 --- a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap +++ b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap @@ -36,6 +36,6 @@ Const = forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type Diagnostics -error[TooManyBinders] at 92..126: Too many binders: expected 2, got 3 -error[TooManyBinders] at 206..247: Too many binders: expected 2, got 3 -error[TooManyBinders] at 308..329: Too many binders: expected 2, got 3 +error[TooManyBinders] at 94..126: Too many binders: expected 2, got 3 +error[TooManyBinders] at 208..247: Too many binders: expected 2, got 3 +error[TooManyBinders] at 310..329: Too many binders: expected 2, got 3 diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index 0b744ecc..80e041ad 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -12,8 +12,8 @@ threeWayConflict :: Int -> Int Types Diagnostics -error[CannotUnify] at 160..248: Cannot unify 'Int' with 'String' -error[CannotUnify] at 160..248: Cannot unify 'String' with 'Int' -error[CannotUnify] at 353..376: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' -error[CannotUnify] at 520..522: Cannot unify 'Int' with 'String' -error[CannotUnify] at 659..666: Cannot unify 'String' with 'Int' +error[CannotUnify] at 163..248: Cannot unify 'Int' with 'String' +error[CannotUnify] at 163..248: Cannot unify 'String' with 'Int' +error[CannotUnify] at 356..376: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' +error[CannotUnify] at 521..522: Cannot unify 'Int' with 'String' +error[CannotUnify] at 660..666: Cannot unify 'String' with 'Int' diff --git a/tests-integration/fixtures/checking/084_instance_eq/Main.snap b/tests-integration/fixtures/checking/084_instance_eq/Main.snap index 459e81eb..995de79f 100644 --- a/tests-integration/fixtures/checking/084_instance_eq/Main.snap +++ b/tests-integration/fixtures/checking/084_instance_eq/Main.snap @@ -17,4 +17,4 @@ instance Eq (Int :: Type) chain: 0 Diagnostics -error[NoInstanceFound] at 101..138: No instance found for: Eq String +error[NoInstanceFound] at 103..138: No instance found for: Eq String diff --git a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap index ba203e7d..55ed18f4 100644 --- a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap +++ b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap @@ -24,4 +24,4 @@ Classes class Eq (&0 :: Type) Diagnostics -error[NoInstanceFound] at 77..149: No instance found for: Eq Foo +error[NoInstanceFound] at 127..149: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap index cae94bab..43cd583e 100644 --- a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap +++ b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap @@ -16,5 +16,5 @@ class Read (&0 :: Type) class Show (&0 :: Type) Diagnostics -error[AmbiguousConstraint] at 101..257: Ambiguous constraint: Show ?5[:0] -error[AmbiguousConstraint] at 101..257: Ambiguous constraint: Read ?5[:0] +error[AmbiguousConstraint] at 235..257: Ambiguous constraint: Show ?5[:0] +error[AmbiguousConstraint] at 235..257: Ambiguous constraint: Read ?5[:0] diff --git a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap index 5922231a..2f20d1fa 100644 --- a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap +++ b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap @@ -24,4 +24,4 @@ Classes class Eq (&0 :: Type) Diagnostics -error[NoInstanceFound] at 77..227: No instance found for: Eq Foo +error[NoInstanceFound] at 191..227: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap index a4da9e47..8cdb2334 100644 --- a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap @@ -20,4 +20,4 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 196..222: No instance found for: Lacks @Type "b" ( a :: Int, b :: String ) +error[NoInstanceFound] at 198..222: No instance found for: Lacks @Type "b" ( a :: Int, b :: String ) diff --git a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap index 766e24a1..df98ccc9 100644 --- a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap @@ -20,4 +20,4 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 152..178: No instance found for: Add 2 3 10 +error[NoInstanceFound] at 154..178: No instance found for: Add 2 3 10 diff --git a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap index f1506305..57fc47d3 100644 --- a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap @@ -20,4 +20,4 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 152..178: No instance found for: Mul 2 3 10 +error[NoInstanceFound] at 154..178: No instance found for: Mul 2 3 10 diff --git a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap index 99227aa5..fdc222ff 100644 --- a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap @@ -21,4 +21,4 @@ Proxy = [Phantom] Diagnostics error[InvalidImportItem] at 64..68: Cannot import item 'kind' -error[NoInstanceFound] at 197..223: No instance found for: Compare 5 1 LT +error[NoInstanceFound] at 199..223: No instance found for: Compare 5 1 LT diff --git a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap index 65c22ffb..d3ebec02 100644 --- a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap @@ -20,4 +20,4 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 162..188: No instance found for: ToString 42 "999" +error[NoInstanceFound] at 164..188: No instance found for: ToString 42 "999" diff --git a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap index 7fa0496d..062a534e 100644 --- a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap +++ b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap @@ -19,5 +19,5 @@ Roles Effect = [Nominal] Diagnostics -error[EmptyDoBlock] at 270..273: Empty do block -error[CannotUnify] at 262..273: Cannot unify 'Type' with '???' +error[EmptyDoBlock] at 271..273: Empty do block +error[CannotUnify] at 264..273: Cannot unify 'Type' with '???' diff --git a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap index dc98a6f4..c6a4737d 100644 --- a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap +++ b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap @@ -20,5 +20,5 @@ Roles Effect = [Nominal] Diagnostics -error[EmptyAdoBlock] at 261..265: Empty ado block -error[CannotUnify] at 252..265: Cannot unify 'Type' with '???' +error[EmptyAdoBlock] at 262..265: Empty ado block +error[CannotUnify] at 254..265: Cannot unify 'Type' with '???' diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 246c6744..ccf632a1 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -73,4 +73,4 @@ class Applicative (&0 :: Type -> Type) <= Bind (&0 :: Type -> Type) class Bind (&0 :: Type -> Type) <= Monad (&0 :: Type -> Type) Diagnostics -error[NoInstanceFound] at 833..878: No instance found for: Discard (&0 :: Type -> Type) +error[NoInstanceFound] at 835..878: No instance found for: Discard (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap index bb89c061..d683506c 100644 --- a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap +++ b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap @@ -36,4 +36,4 @@ derive forall (&0 :: Type). Eq (Proxy @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type derive Eq (ContainsNoEq :: Type) Diagnostics -error[NoInstanceFound] at 157..190: No instance found for: Eq NoEq +error[NoInstanceFound] at 159..190: No instance found for: Eq NoEq diff --git a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap index 60e279d4..961735a6 100644 --- a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap @@ -28,4 +28,4 @@ Derived derive Eq (Box :: Type) Diagnostics -error[NoInstanceFound] at 87..111: No instance found for: Eq NoEq +error[NoInstanceFound] at 89..111: No instance found for: Eq NoEq diff --git a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap index c7742fcd..b5766977 100644 --- a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap @@ -35,4 +35,4 @@ derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type derive Eq (&1 :: Type) => Eq (WrapNoEq1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 214..258: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound] at 216..258: No instance found for: Eq1 (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap index 583d0c4f..a0ceb80b 100644 --- a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap +++ b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap @@ -23,4 +23,4 @@ derive Eq (&0 :: Type) => Eq (Either Int (&0 :: Type) :: Type) derive Eq (Either Int (&0 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 123..158: No instance found for: Eq (&0 :: Type) +error[NoInstanceFound] at 125..158: No instance found for: Eq (&0 :: Type) diff --git a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap index c456ea9e..809dbb7c 100644 --- a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap +++ b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap @@ -48,4 +48,4 @@ derive Eq (ContainsNoOrd :: Type) derive Ord (ContainsNoOrd :: Type) Diagnostics -error[NoInstanceFound] at 350..384: No instance found for: Ord NoOrd +error[NoInstanceFound] at 351..384: No instance found for: Ord NoOrd diff --git a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap index 4d2d5d3f..39e64ab7 100644 --- a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap @@ -37,5 +37,5 @@ derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (WrapNoOrd1 @Type (&0 : derive Ord (&1 :: Type) => Ord (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 319..365: No instance found for: Ord1 (&0 :: Type -> Type) -error[NoInstanceFound] at 319..365: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound] at 320..365: No instance found for: Ord1 (&0 :: Type -> Type) +error[NoInstanceFound] at 320..365: No instance found for: Eq1 (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap index b1876c56..75494040 100644 --- a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap @@ -56,7 +56,7 @@ derive Eq1 (&0 :: Type -> Type) => Eq (U (&0 :: Type -> Type) :: Type) derive Ord1 (&0 :: Type -> Type) => Ord (U (&0 :: Type -> Type) :: Type) Diagnostics -error[NoInstanceFound] at 123..162: No instance found for: Eq ((&1 :: Type -> Type) Int) -error[NoInstanceFound] at 162..202: No instance found for: Ord ((&1 :: Type -> Type) Int) -error[NoInstanceFound] at 444..483: No instance found for: Eq ((&1 :: Int -> Type) 42) -error[NoInstanceFound] at 483..523: No instance found for: Ord ((&1 :: Int -> Type) 42) +error[NoInstanceFound] at 125..162: No instance found for: Eq ((&1 :: Type -> Type) Int) +error[NoInstanceFound] at 163..202: No instance found for: Ord ((&1 :: Type -> Type) Int) +error[NoInstanceFound] at 446..483: No instance found for: Eq ((&1 :: Int -> Type) 42) +error[NoInstanceFound] at 484..523: No instance found for: Ord ((&1 :: Int -> Type) 42) diff --git a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap index 672fe1ed..832a20e9 100644 --- a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap @@ -18,4 +18,4 @@ Roles Foo = [] Diagnostics -error[ExpectedNewtype] at 68..102: Expected a newtype, got: Foo +error[ExpectedNewtype] at 70..102: Expected a newtype, got: Foo diff --git a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap index df4ebac2..5bfd22fc 100644 --- a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap @@ -21,4 +21,4 @@ Derived derive Show (Identity String :: Type) Diagnostics -error[NoInstanceFound] at 81..129: No instance found for: Show String +error[NoInstanceFound] at 83..129: No instance found for: Show String diff --git a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap index 6d4ac5b3..638aee67 100644 --- a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap +++ b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap @@ -21,4 +21,4 @@ Derived derive Show (Identity (&0 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 81..124: No instance found for: Show (&0 :: Type) +error[NoInstanceFound] at 83..124: No instance found for: Show (&0 :: Type) diff --git a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap index dbf55688..414e82f9 100644 --- a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap @@ -35,4 +35,4 @@ derive Functor (&0 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type -> Type) derive Functor (WrapNoFunctor @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 174..216: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound] at 175..216: No instance found for: Functor (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap index 15e8b55a..f832f53e 100644 --- a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap @@ -40,4 +40,4 @@ derive Functor (Reader (&0 :: Type) :: Type -> Type) derive Functor (Cont (&0 :: Type) :: Type -> Type) Diagnostics -error[ContravariantOccurrence] at 99..133: Type variable occurs in contravariant position: (~&0 :: Type) +error[ContravariantOccurrence] at 100..133: Type variable occurs in contravariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap index ac516733..16b72bf3 100644 --- a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap @@ -51,5 +51,5 @@ derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Bifunctor derive Bifunctor (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound] at 277..330: No instance found for: Functor (&0 :: Type -> Type) -error[NoInstanceFound] at 277..330: No instance found for: Functor (&1 :: Type -> Type) +error[NoInstanceFound] at 278..330: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound] at 278..330: No instance found for: Functor (&1 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap index 1284d420..aa3d550d 100644 --- a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap +++ b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap @@ -32,5 +32,5 @@ Derived derive Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[DeriveMissingFunctor] at 104..145: Deriving Functor requires Data.Functor to be in scope -error[DeriveMissingFunctor] at 104..145: Deriving Functor requires Data.Functor to be in scope +error[DeriveMissingFunctor] at 105..145: Deriving Functor requires Data.Functor to be in scope +error[DeriveMissingFunctor] at 105..145: Deriving Functor requires Data.Functor to be in scope diff --git a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap index 2465de7b..90e14254 100644 --- a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap @@ -29,5 +29,5 @@ derive Contravariant (Identity :: Type -> Type) derive Contravariant (Producer :: Type -> Type) Diagnostics -error[CovariantOccurrence] at 168..207: Type variable occurs in covariant position: (~&0 :: Type) -error[CovariantOccurrence] at 308..347: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence] at 169..207: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence] at 309..347: Type variable occurs in covariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap index 2649fe85..c1c34a0b 100644 --- a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap @@ -32,6 +32,6 @@ derive Profunctor (WrongFirst :: Type -> Type -> Type) derive Profunctor (WrongSecond :: Type -> Type -> Type) Diagnostics -error[CovariantOccurrence] at 150..188: Type variable occurs in covariant position: (~&0 :: Type) -error[ContravariantOccurrence] at 290..329: Type variable occurs in contravariant position: (~&1 :: Type) -error[CovariantOccurrence] at 290..329: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence] at 151..188: Type variable occurs in covariant position: (~&0 :: Type) +error[ContravariantOccurrence] at 291..329: Type variable occurs in contravariant position: (~&1 :: Type) +error[CovariantOccurrence] at 291..329: Type variable occurs in covariant position: (~&0 :: Type) diff --git a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap index 294eb78f..d4d07e0a 100644 --- a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap @@ -23,5 +23,5 @@ Derived derive Bifunctor (Triple Int String :: Type -> Type) Diagnostics -error[CannotUnify] at 118..138: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 92..138: Cannot derive for type: Triple Int String +error[CannotUnify] at 119..138: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 93..138: Cannot derive for type: Triple Int String diff --git a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap index b14639e7..468fc94b 100644 --- a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap @@ -29,7 +29,7 @@ derive Functor (Pair Int String :: Type) derive Functor (Unit :: Type) Diagnostics -error[CannotUnify] at 104..122: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 80..122: Cannot derive for type: Pair Int String -error[CannotUnify] at 164..169: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 140..169: Cannot derive for type: Unit +error[CannotUnify] at 105..122: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 81..122: Cannot derive for type: Pair Int String +error[CannotUnify] at 165..169: Cannot unify 'Type' with 'Type -> Type' +error[CannotDeriveForType] at 141..169: Cannot derive for type: Unit diff --git a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap index 2210f2c3..7f4852c0 100644 --- a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap @@ -35,4 +35,4 @@ derive Foldable (&0 :: Type -> Type) => Foldable (Wrap @Type (&0 :: Type -> Type derive Foldable (WrapNoFoldable @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 180..224: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound] at 181..224: No instance found for: Foldable (&0 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap index e1f05b43..13ac9648 100644 --- a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap @@ -51,5 +51,5 @@ derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Bifolda derive Bifoldable (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound] at 284..338: No instance found for: Foldable (&0 :: Type -> Type) -error[NoInstanceFound] at 284..338: No instance found for: Foldable (&1 :: Type -> Type) +error[NoInstanceFound] at 285..338: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound] at 285..338: No instance found for: Foldable (&1 :: Type -> Type) diff --git a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap index d1962ab7..70a1cc4f 100644 --- a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap +++ b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap @@ -30,5 +30,5 @@ Derived derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Traversable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 174..250: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) -error[NoInstanceFound] at 174..250: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound] at 175..250: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound] at 175..250: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index 58cae96f..16fe4582 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -97,5 +97,5 @@ derive Eq1 (Rec :: Type -> Type) derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 635..753: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 916..942: No instance found for: Eq (NoEq (~&0 :: Type)) +error[NoInstanceFound] at 711..753: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 918..942: No instance found for: Eq (NoEq (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index 3f00c8e1..1a2f4a4f 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -68,6 +68,6 @@ derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: T derive Ord1 (&0 :: Type -> Type) => Ord1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 591..707: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 707..752: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 877..904: No instance found for: Ord (NoOrd (~&0 :: Type)) +error[NoInstanceFound] at 665..707: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 708..752: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound] at 878..904: No instance found for: Ord (NoOrd (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap index f5197258..01166a71 100644 --- a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap @@ -18,4 +18,4 @@ Roles NotANewtype = [] Diagnostics -error[ExpectedNewtype] at 90..129: Expected a newtype, got: NotANewtype +error[ExpectedNewtype] at 92..129: Expected a newtype, got: NotANewtype diff --git a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap index 7c82a000..af62b2c8 100644 --- a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap +++ b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap @@ -21,4 +21,4 @@ Roles F = [Representational, Nominal] Diagnostics -error[InvalidRoleDeclaration] at 17..39: Invalid role declaration: declared Phantom, inferred Nominal +error[InvalidRoleDeclaration] at 19..39: Invalid role declaration: declared Phantom, inferred Nominal diff --git a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap index f321e1fe..857f48c7 100644 --- a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap +++ b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap @@ -28,4 +28,4 @@ Maybe = [Representational] Either = [Representational, Representational] Diagnostics -error[NoInstanceFound] at 114..165: No instance found for: Coercible @Type (Maybe Int) (Either Int String) +error[NoInstanceFound] at 116..165: No instance found for: Coercible @Type (Maybe Int) (Either Int String) diff --git a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap index 47b992da..a4d05fc9 100644 --- a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap +++ b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap @@ -13,4 +13,4 @@ Roles Nominal = [Nominal] Diagnostics -error[NoInstanceFound] at 194..251: No instance found for: Coercible @Type (Nominal Int) (Nominal String) +error[NoInstanceFound] at 196..251: No instance found for: Coercible @Type (Nominal Int) (Nominal String) diff --git a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap index 5116f9f8..1a7711a4 100644 --- a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap +++ b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap @@ -9,7 +9,7 @@ coerceQualified :: Int -> HiddenAge Types Diagnostics -error[CoercibleConstructorNotInScope] at 85..119: Constructor not in scope for Coercible -error[NoInstanceFound] at 85..119: No instance found for: Coercible @Type Int HiddenAge -error[CoercibleConstructorNotInScope] at 141..180: Constructor not in scope for Coercible -error[NoInstanceFound] at 141..180: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope] at 87..119: Constructor not in scope for Coercible +error[NoInstanceFound] at 87..119: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope] at 143..180: Constructor not in scope for Coercible +error[NoInstanceFound] at 143..180: No instance found for: Coercible @Type Int HiddenAge diff --git a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap index 75ca5996..2b77a5be 100644 --- a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap +++ b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap @@ -8,5 +8,5 @@ coerceOpen :: Int -> HiddenAge Types Diagnostics -error[CoercibleConstructorNotInScope] at 57..89: Constructor not in scope for Coercible -error[NoInstanceFound] at 57..89: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope] at 59..89: Constructor not in scope for Coercible +error[NoInstanceFound] at 59..89: No instance found for: Coercible @Type Int HiddenAge diff --git a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap index 7fe6b950..a60be456 100644 --- a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap +++ b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap @@ -30,4 +30,4 @@ List = [Representational] Container = [Representational] Diagnostics -error[NoInstanceFound] at 209..272: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) +error[NoInstanceFound] at 211..272: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) diff --git a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap index 14cb0f01..a9108932 100644 --- a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap +++ b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap @@ -12,10 +12,10 @@ invalidEq :: Proxy @(Row Int) ( left :: 1, right :: 5 ) Types Diagnostics -error[NoInstanceFound] at 49..182: No instance found for: Compare 10 5 LT -error[NoInstanceFound] at 49..182: No instance found for: Compare 5 1 LT -error[NoInstanceFound] at 226..359: No instance found for: Compare 1 5 GT -error[NoInstanceFound] at 226..359: No instance found for: Compare 5 10 GT -error[NoInstanceFound] at 403..488: No instance found for: Compare 5 1 LT -error[NoInstanceFound] at 513..559: No instance found for: Compare 1 5 GT -error[NoInstanceFound] at 585..631: No instance found for: Compare 1 5 EQ +error[NoInstanceFound] at 132..182: No instance found for: Compare 10 5 LT +error[NoInstanceFound] at 132..182: No instance found for: Compare 5 1 LT +error[NoInstanceFound] at 309..359: No instance found for: Compare 1 5 GT +error[NoInstanceFound] at 309..359: No instance found for: Compare 5 10 GT +error[NoInstanceFound] at 444..488: No instance found for: Compare 5 1 LT +error[NoInstanceFound] at 515..559: No instance found for: Compare 1 5 GT +error[NoInstanceFound] at 587..631: No instance found for: Compare 1 5 EQ diff --git a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap index bc4a790e..8d9c8ebe 100644 --- a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap +++ b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap @@ -48,12 +48,12 @@ Roles Proxy = [Phantom] Diagnostics -warning[CustomWarning] at 241..262: This function is deprecated -warning[CustomWarning] at 386..408: Left Right -warning[CustomWarning] at 533..554: Line 1 +warning[CustomWarning] at 243..262: This function is deprecated +warning[CustomWarning] at 388..408: Left Right +warning[CustomWarning] at 535..554: Line 1 Line 2 -warning[CustomWarning] at 688..715: Got type: Int -warning[CustomWarning] at 860..886: Label: myField -warning[CustomWarning] at 1052..1084: Label: "h e l l o" -warning[CustomWarning] at 1258..1289: Label: "hel\"lo" -warning[CustomWarning] at 1465..1494: Label: """raw\nstring""" +warning[CustomWarning] at 690..715: Got type: Int +warning[CustomWarning] at 862..886: Label: myField +warning[CustomWarning] at 1054..1084: Label: "h e l l o" +warning[CustomWarning] at 1260..1289: Label: "hel\"lo" +warning[CustomWarning] at 1467..1494: Label: """raw\nstring""" diff --git a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap index e5be9930..5da56926 100644 --- a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap +++ b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap @@ -27,6 +27,6 @@ Roles Proxy = [Phantom] Diagnostics -error[CustomFailure] at 231..252: This operation is not allowed -error[CustomFailure] at 409..441: Error: +error[CustomFailure] at 233..252: This operation is not allowed +error[CustomFailure] at 411..441: Error: Type String From 8538790d0c04e5f4cdcc56ffdaef4e88e65d44b1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 01:41:40 +0800 Subject: [PATCH 011/386] Add sourceCommand to VS Code extension --- vscode/package.json | 14 +++++++++++++- vscode/src/extension.ts | 11 ++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index 80512b7e..0d70af15 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -46,5 +46,17 @@ }, "extensionDependencies": [ "nwolverson.language-purescript" - ] + ], + "contributes": { + "configuration": { + "title": "PureScript Analyzer", + "properties": { + "purescriptAnalyzer.sourceCommand": { + "type": "string", + "default": null, + "description": "Command to use to get source files. Setting this also disables the spago.lock integration." + } + } + } + } } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 1b152059..aa9f1975 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -1,4 +1,4 @@ -import { ExtensionContext } from "vscode"; +import { ExtensionContext, workspace } from "vscode"; import { LanguageClient, @@ -10,8 +10,17 @@ import { let client: LanguageClient; export function activate(context: ExtensionContext) { + const config = workspace.getConfiguration("purescriptAnalyzer"); + const sourceCommand = config.get("sourceCommand"); + + const args: string[] = []; + if (sourceCommand) { + args.push("--source-command", sourceCommand); + } + const serverOptions: ServerOptions = { command: "purescript-analyzer", + args, transport: TransportKind.stdio, }; From 00aba0525cab2e45186ff6114d5233af9f7843c0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 03:26:57 +0800 Subject: [PATCH 012/386] Extract decision logic, add snapshot commands --- .claude/skills/type-checker-tests/SKILL.md | 14 +- compiler-scripts/src/main.rs | 26 +- compiler-scripts/src/test_runner.rs | 88 ++++++- compiler-scripts/src/test_runner/cli.rs | 29 ++- compiler-scripts/src/test_runner/decision.rs | 211 ++++++++++++++++ compiler-scripts/src/test_runner/nextest.rs | 9 +- compiler-scripts/src/test_runner/pending.rs | 184 ++++++++++++-- compiler-scripts/src/test_runner/ui.rs | 250 +++++++++++++++---- tests-integration/src/generated/basic.rs | 6 +- 9 files changed, 723 insertions(+), 94 deletions(-) create mode 100644 compiler-scripts/src/test_runner/decision.rs diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index ea5ea199..e0ce0822 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -16,12 +16,17 @@ Use this skill when adding new type checker functions or expanding behavior. |--------|---------| | Find next test number | `ls tests-integration/fixtures/checking/ \| tail -5` | | Run a test or multiple tests | `just t checking NNN` or `just t c 101 102` | +| Run with full diffs | `just t checking NNN --diff` | | Run with tracing enabled | `just t checking --debug NNN` | | Run all checking tests | `just t checking` or `just t c` | | Accept all pending snapshots | `cargo insta accept` | Use `just t checking --help` for all options. +**Output modes:** +- Default: Shows summary only (snapshot paths + line counts like `+5, -3`) +- `--diff`: Shows full inline diffs for each snapshot + ## Creating a Test ### 1. Create fixture directory @@ -55,12 +60,15 @@ test' [x] = x ### 3. Generate and review snapshot ```bash -just t checking NNN +just t checking NNN # summary only +just t checking NNN --diff # full diff output ``` This outputs: -- `CREATED path` (green) with numbered lines showing full content -- `UPDATED path` (yellow) with chunked diff (2 lines context, line numbers) +- `CREATED path (+N)` (green) showing new snapshot with line count +- `UPDATED path (+N, -M)` (yellow) showing changed lines count + +Use `--diff` to see full inline diffs when reviewing changes. ## Multi-File Tests diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs index 96862b1c..e6d7b12f 100644 --- a/compiler-scripts/src/main.rs +++ b/compiler-scripts/src/main.rs @@ -1,5 +1,7 @@ use clap::Parser; -use compiler_scripts::test_runner::{RunArgs, TestCategory, run_category}; +use compiler_scripts::test_runner::{ + CategoryCommand, RunArgs, TestCategory, accept_category, reject_category, run_category, +}; #[derive(Parser)] #[command(about = "Compiler development scripts")] @@ -13,9 +15,25 @@ struct Cli { fn main() { let cli = Cli::parse(); - let outcome = run_category(cli.category, &cli.args); - if !outcome.tests_passed { - std::process::exit(1); + match &cli.args.command { + Some(CategoryCommand::Accept(args)) => { + let outcome = accept_category(cli.category, args); + if !outcome.success { + std::process::exit(1); + } + } + Some(CategoryCommand::Reject(args)) => { + let outcome = reject_category(cli.category, args); + if !outcome.success { + std::process::exit(1); + } + } + None => { + let outcome = run_category(cli.category, &cli.args); + if !outcome.tests_passed { + std::process::exit(1); + } + } } } diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs index 3c044026..b1166089 100644 --- a/compiler-scripts/src/test_runner.rs +++ b/compiler-scripts/src/test_runner.rs @@ -1,18 +1,21 @@ mod category; mod cli; +mod decision; mod nextest; mod pending; mod traces; mod ui; pub use category::TestCategory; -pub use cli::RunArgs; +pub use cli::{CategoryCommand, RunArgs, SnapshotArgs}; use std::path::PathBuf; use std::time::Instant; use console::style; +use crate::test_runner::ui::NextActionsArgs; + pub struct TestOutcome { pub tests_passed: bool, pub pending_count: usize, @@ -32,17 +35,82 @@ pub fn run_category(category: TestCategory, args: &RunArgs) -> TestOutcome { let trace_paths = traces::collect_trace_paths(&args.filters, args.debug); // 4. Process pending snapshots - let pending_count = pending::process_pending_snapshots(category, args, &trace_paths); + let pending_result = pending::process_pending_snapshots(category, args, &trace_paths); // 5. Print next actions - ui::print_next_actions( - category.as_str(), - &args.filters, + ui::print_next_actions(NextActionsArgs { + category_name: category.as_str(), + filters: &args.filters, tests_passed, - pending_count, - &trace_paths, - args.debug, - ); + pending_count: pending_result.count, + total_lines_changed: pending_result.total_lines_changed, + trace_paths: &trace_paths, + debug: args.debug, + showed_diffs: args.diff, + }); + + TestOutcome { tests_passed, pending_count: pending_result.count, trace_paths } +} + +pub struct SnapshotOutcome { + pub success: bool, + pub count: usize, +} + +pub fn accept_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotOutcome { + let snapshots = pending::collect_pending_snapshots(category, &args.filters); + + if snapshots.is_empty() { + println!("{}", style("No pending snapshots found.").dim()); + return SnapshotOutcome { success: true, count: 0 }; + } + + if !args.all && args.filters.is_empty() { + println!("{} pending snapshot(s) in {}", snapshots.len(), style(category.as_str()).cyan()); + println!(); + for info in &snapshots { + println!(" {}", style(&info.short_path).dim()); + } + println!(); + println!( + "To accept all, run: {}", + style(format!("just t {} accept --all", category.as_str())).cyan() + ); + return SnapshotOutcome { success: false, count: 0 }; + } + + let result = pending::accept_snapshots(&snapshots); + println!(); + println!("{}", style(format!("Accepted {} snapshot(s)", result.accepted)).green()); + + SnapshotOutcome { success: result.failed == 0, count: result.accepted } +} + +pub fn reject_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotOutcome { + let snapshots = pending::collect_pending_snapshots(category, &args.filters); + + if snapshots.is_empty() { + println!("{}", style("No pending snapshots found.").dim()); + return SnapshotOutcome { success: true, count: 0 }; + } + + if !args.all && args.filters.is_empty() { + println!("{} pending snapshot(s) in {}", snapshots.len(), style(category.as_str()).cyan()); + println!(); + for info in &snapshots { + println!(" {}", style(&info.short_path).dim()); + } + println!(); + println!( + "To reject all, run: {}", + style(format!("just t {} reject --all", category.as_str())).cyan() + ); + return SnapshotOutcome { success: false, count: 0 }; + } + + let result = pending::reject_snapshots(&snapshots); + println!(); + println!("{}", style(format!("Rejected {} snapshot(s)", result.rejected)).red()); - TestOutcome { tests_passed, pending_count, trace_paths } + SnapshotOutcome { success: result.failed == 0, count: result.rejected } } diff --git a/compiler-scripts/src/test_runner/cli.rs b/compiler-scripts/src/test_runner/cli.rs index 8ffa86b4..f0aabd4c 100644 --- a/compiler-scripts/src/test_runner/cli.rs +++ b/compiler-scripts/src/test_runner/cli.rs @@ -1,7 +1,30 @@ -use clap::Args; +use clap::{Args, Subcommand}; + +#[derive(Subcommand, Clone, Debug)] +pub enum CategoryCommand { + /// Accept pending snapshots for this category + Accept(SnapshotArgs), + /// Reject pending snapshots for this category + Reject(SnapshotArgs), +} + +#[derive(Args, Clone, Debug)] +pub struct SnapshotArgs { + /// Snapshot filters (same as test filters) + #[arg(num_args = 0..)] + pub filters: Vec, + + /// Accept/reject all pending snapshots (required when no filters provided) + #[arg(long)] + pub all: bool, +} #[derive(Args, Clone, Debug)] pub struct RunArgs { + /// Subcommand (accept/reject) or test filters + #[command(subcommand)] + pub command: Option, + /// Test name or number filters (passed through to nextest) #[arg(num_args = 0..)] pub filters: Vec, @@ -13,4 +36,8 @@ pub struct RunArgs { /// Enable tracing output for debugging #[arg(long)] pub debug: bool, + + /// Show full diffs (by default only shows summary to reduce output) + #[arg(long)] + pub diff: bool, } diff --git a/compiler-scripts/src/test_runner/decision.rs b/compiler-scripts/src/test_runner/decision.rs new file mode 100644 index 00000000..1713a96d --- /dev/null +++ b/compiler-scripts/src/test_runner/decision.rs @@ -0,0 +1,211 @@ +/// Decision logic for test runner output, separated from rendering for testability. + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecisionInput { + pub tests_passed: bool, + pub pending_count: usize, + pub total_lines_changed: usize, + pub showed_diffs: bool, + pub ran_all: bool, + pub debug: bool, + pub trace_count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Outcome { + Clean, + Failure(FailureDecision), + Pending(PendingDecision), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FailureDecision { + pub show_debug_hint: bool, + pub show_trace_hint: bool, + pub max_traces_to_show: usize, + pub pending_note: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PendingDecision { + pub show_lines_changed: bool, + pub next_action: NextAction, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NextAction { + AcceptOrReject, + ReviewSubset, + ShowDiff, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SnapshotDisplayLimits { + pub max_shown: usize, +} + +pub fn decide_outcome(input: &DecisionInput) -> Outcome { + let has_pending = input.pending_count > 0; + let has_traces = input.trace_count > 0; + + if input.tests_passed && !has_pending { + return Outcome::Clean; + } + + if !input.tests_passed { + return Outcome::Failure(FailureDecision { + show_debug_hint: !input.debug, + show_trace_hint: input.debug && has_traces, + max_traces_to_show: 3, + pending_note: if has_pending { Some(input.pending_count) } else { None }, + }); + } + + let many_pending = input.ran_all && input.pending_count > 3; + + let next_action = if input.showed_diffs { + NextAction::AcceptOrReject + } else if many_pending { + NextAction::ReviewSubset + } else { + NextAction::ShowDiff + }; + + Outcome::Pending(PendingDecision { + show_lines_changed: input.total_lines_changed > 50, + next_action, + }) +} + +pub fn decide_snapshot_limits(input: &DecisionInput) -> SnapshotDisplayLimits { + let many_pending = input.ran_all && input.pending_count > 3; + + let max_shown = if input.showed_diffs { + usize::MAX + } else if many_pending { + 2 + } else { + 5 + }; + + SnapshotDisplayLimits { max_shown } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn base_input() -> DecisionInput { + DecisionInput { + tests_passed: true, + pending_count: 0, + total_lines_changed: 0, + showed_diffs: false, + ran_all: true, + debug: false, + trace_count: 0, + } + } + + #[test] + fn clean_when_passed_no_pending() { + let input = base_input(); + assert_eq!(decide_outcome(&input), Outcome::Clean); + } + + #[test] + fn failure_suggests_debug_when_not_in_debug_mode() { + let input = DecisionInput { tests_passed: false, ..base_input() }; + let Outcome::Failure(decision) = decide_outcome(&input) else { + panic!("expected Failure"); + }; + assert!(decision.show_debug_hint); + assert!(!decision.show_trace_hint); + } + + #[test] + fn failure_suggests_traces_when_in_debug_mode_with_traces() { + let input = + DecisionInput { tests_passed: false, debug: true, trace_count: 2, ..base_input() }; + let Outcome::Failure(decision) = decide_outcome(&input) else { + panic!("expected Failure"); + }; + assert!(!decision.show_debug_hint); + assert!(decision.show_trace_hint); + } + + #[test] + fn failure_notes_pending_count() { + let input = DecisionInput { tests_passed: false, pending_count: 5, ..base_input() }; + let Outcome::Failure(decision) = decide_outcome(&input) else { + panic!("expected Failure"); + }; + assert_eq!(decision.pending_note, Some(5)); + } + + #[test] + fn pending_accept_reject_after_diff() { + let input = DecisionInput { pending_count: 1, showed_diffs: true, ..base_input() }; + let Outcome::Pending(decision) = decide_outcome(&input) else { + panic!("expected Pending"); + }; + assert_eq!(decision.next_action, NextAction::AcceptOrReject); + } + + #[test] + fn pending_review_subset_when_many() { + let input = DecisionInput { pending_count: 10, ran_all: true, ..base_input() }; + let Outcome::Pending(decision) = decide_outcome(&input) else { + panic!("expected Pending"); + }; + assert_eq!(decision.next_action, NextAction::ReviewSubset); + } + + #[test] + fn pending_show_diff_for_small_batch() { + let input = DecisionInput { pending_count: 2, ran_all: true, ..base_input() }; + let Outcome::Pending(decision) = decide_outcome(&input) else { + panic!("expected Pending"); + }; + assert_eq!(decision.next_action, NextAction::ShowDiff); + } + + #[test] + fn pending_shows_lines_changed_when_large() { + let input = DecisionInput { pending_count: 1, total_lines_changed: 100, ..base_input() }; + let Outcome::Pending(decision) = decide_outcome(&input) else { + panic!("expected Pending"); + }; + assert!(decision.show_lines_changed); + } + + #[test] + fn snapshot_limits_max_when_diff_enabled() { + let input = DecisionInput { showed_diffs: true, ..base_input() }; + let limits = decide_snapshot_limits(&input); + assert_eq!(limits.max_shown, usize::MAX); + } + + #[test] + fn snapshot_limits_2_when_many_pending() { + let input = DecisionInput { pending_count: 10, ran_all: true, ..base_input() }; + let limits = decide_snapshot_limits(&input); + assert_eq!(limits.max_shown, 2); + } + + #[test] + fn snapshot_limits_5_default() { + let input = DecisionInput { pending_count: 2, ..base_input() }; + let limits = decide_snapshot_limits(&input); + assert_eq!(limits.max_shown, 5); + } + + #[test] + fn filtered_run_not_considered_many() { + let input = DecisionInput { pending_count: 10, ran_all: false, ..base_input() }; + let Outcome::Pending(decision) = decide_outcome(&input) else { + panic!("expected Pending"); + }; + assert_eq!(decision.next_action, NextAction::ShowDiff); + } +} diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs index a960e204..d3c4e0e7 100644 --- a/compiler-scripts/src/test_runner/nextest.rs +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -59,8 +59,13 @@ pub fn run_nextest( if !status.success() { eprintln!("{}", style("Tests failed, re-running verbose...").yellow()); - let verbose_args = - RunArgs { filters: args.filters.clone(), verbose: true, debug: args.debug }; + let verbose_args = RunArgs { + command: None, + filters: args.filters.clone(), + verbose: true, + debug: args.debug, + diff: args.diff, + }; let mut retry = build_nextest_command(category, &verbose_args, fixture_hashes); let _ = retry.status(); } diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs index f0a5da9c..6cd6bdef 100644 --- a/compiler-scripts/src/test_runner/pending.rs +++ b/compiler-scripts/src/test_runner/pending.rs @@ -1,23 +1,37 @@ use std::env; +use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use console::style; use serde::Deserialize; use crate::test_runner::category::TestCategory; use crate::test_runner::cli::RunArgs; +use crate::test_runner::decision; +use crate::test_runner::decision::DecisionInput; use crate::test_runner::ui; #[derive(Deserialize)] -struct PendingSnapshot { +struct PendingSnapshotJson { path: String, } -pub fn process_pending_snapshots( - category: TestCategory, - args: &RunArgs, - trace_paths: &[PathBuf], -) -> usize { +pub struct PendingResult { + pub count: usize, + pub total_lines_changed: usize, +} + +pub struct SnapshotInfo { + pub snapshot_path: String, + pub short_path: String, + pub snap_new: PathBuf, + pub is_update: bool, + pub trace_path: Option, +} + +/// Collect pending snapshots for a category, optionally filtered. +pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> Vec { let pending_output = Command::new("cargo") .arg("insta") .arg("pending-snapshots") @@ -32,14 +46,16 @@ pub fn process_pending_snapshots( let cwd = env::current_dir().unwrap(); let fixtures_fragment = category.fixtures_subdir_fragment(); - let mut count = 0; + let mut snapshots = Vec::new(); + for line in pending.lines() { let line = line.trim(); if line.is_empty() { continue; } - let snapshot_path = if let Ok(snapshot) = serde_json::from_str::(line) { + let snapshot_path = if let Ok(snapshot) = serde_json::from_str::(line) + { snapshot.path } else { continue; @@ -49,28 +65,160 @@ pub fn process_pending_snapshots( continue; } - if !args.filters.is_empty() && !args.filters.iter().any(|f| snapshot_path.contains(f)) { + if !filters.is_empty() && !filters.iter().any(|f| snapshot_path.contains(f)) { continue; } let short_path = snapshot_path .strip_prefix(cwd.to_str().unwrap_or("")) .unwrap_or(&snapshot_path) - .trim_start_matches('/'); + .trim_start_matches('/') + .to_string(); - let snap = Path::new(&snapshot_path); let snap_new = PathBuf::from(format!("{}.new", snapshot_path)); - let trace_path = category.trace_for_snapshot(snap, trace_paths); + let is_update = Path::new(&snapshot_path).exists(); + + snapshots.push(SnapshotInfo { + snapshot_path, + short_path, + snap_new, + is_update, + trace_path: None, // Populated separately if needed + }); + } + + snapshots +} - if snap.exists() { - ui::display_snapshot_diff(snap, &snap_new, short_path, trace_path.as_deref()); +pub fn process_pending_snapshots( + category: TestCategory, + args: &RunArgs, + trace_paths: &[PathBuf], +) -> PendingResult { + let mut snapshots = collect_pending_snapshots(category, &args.filters); + + // Populate trace paths + for info in &mut snapshots { + info.trace_path = category.trace_for_snapshot(Path::new(&info.snapshot_path), trace_paths); + } + + let count = snapshots.len(); + let mut total_lines_changed = 0; + + let limits = decision::decide_snapshot_limits(&DecisionInput { + tests_passed: true, // not relevant for snapshot limits + pending_count: count, + total_lines_changed: 0, // not known yet, not relevant for limits + showed_diffs: args.diff, + ran_all: args.filters.is_empty(), + debug: args.debug, + trace_count: trace_paths.len(), + }); + + let max_shown = limits.max_shown; + + for info in snapshots.iter().take(max_shown) { + let snap = Path::new(&info.snapshot_path); + let stats = if info.is_update { + ui::display_snapshot_diff( + snap, + &info.snap_new, + &info.short_path, + info.trace_path.as_deref(), + args.diff, + ) } else { - ui::display_new_snapshot(&snap_new, short_path, trace_path.as_deref()); + ui::display_new_snapshot( + &info.snap_new, + &info.short_path, + info.trace_path.as_deref(), + args.diff, + ) + }; + total_lines_changed += stats.added + stats.removed; + } + + if count > max_shown { + let hidden = count - max_shown; + println!( + "{}", + style(format!("...and {} more pending snapshot(s) not shown", hidden)).dim() + ); + } + + PendingResult { count, total_lines_changed } +} + +pub struct AcceptRejectResult { + pub accepted: usize, + pub rejected: usize, + pub failed: usize, +} + +pub fn accept_snapshots(snapshots: &[SnapshotInfo]) -> AcceptRejectResult { + let mut accepted = 0; + let mut failed = 0; + + for info in snapshots { + if !info.snap_new.exists() { + println!( + "{} {} (missing .new file)", + style("SKIP").yellow().bold(), + style(&info.short_path).cyan() + ); + failed += 1; + continue; + } + + let snap_path = Path::new(&info.snapshot_path); + + // Remove existing snapshot if present + if snap_path.exists() + && let Err(e) = fs::remove_file(snap_path) + { + println!("{} {} ({})", style("FAIL").red().bold(), style(&info.short_path).cyan(), e); + failed += 1; + continue; + } + + // Rename .new to .snap + if let Err(e) = fs::rename(&info.snap_new, snap_path) { + println!("{} {} ({})", style("FAIL").red().bold(), style(&info.short_path).cyan(), e); + failed += 1; + continue; + } + + println!("{} {}", style("ACCEPTED").green().bold(), style(&info.short_path).cyan()); + accepted += 1; + } + + AcceptRejectResult { accepted, rejected: 0, failed } +} + +pub fn reject_snapshots(snapshots: &[SnapshotInfo]) -> AcceptRejectResult { + let mut rejected = 0; + let mut failed = 0; + + for info in snapshots { + if !info.snap_new.exists() { + println!( + "{} {} (missing .new file)", + style("SKIP").yellow().bold(), + style(&info.short_path).cyan() + ); + failed += 1; + continue; + } + + if let Err(e) = fs::remove_file(&info.snap_new) { + println!("{} {} ({})", style("FAIL").red().bold(), style(&info.short_path).cyan(), e); + failed += 1; + continue; } - count += 1; - println!(); + println!("{} {}", style("REJECTED").red().bold(), style(&info.short_path).cyan()); + rejected += 1; } - count + AcceptRejectResult { accepted: 0, rejected, failed } } diff --git a/compiler-scripts/src/test_runner/ui.rs b/compiler-scripts/src/test_runner/ui.rs index 6b8034ea..a4a44397 100644 --- a/compiler-scripts/src/test_runner/ui.rs +++ b/compiler-scripts/src/test_runner/ui.rs @@ -1,98 +1,240 @@ use std::fs; use std::path::{Path, PathBuf}; +use similar::{ChangeTag, TextDiff}; + use crate::console::style; use crate::snapshots::{print_diff, strip_frontmatter}; +use crate::test_runner::decision; +use crate::test_runner::decision::{ + DecisionInput, FailureDecision, NextAction, Outcome, PendingDecision, +}; + +pub struct SnapshotStats { + pub added: usize, + pub removed: usize, +} + +fn count_diff_lines(old: &str, new: &str) -> SnapshotStats { + let diff = TextDiff::from_lines(old, new); + let mut added = 0; + let mut removed = 0; + + for change in diff.iter_all_changes() { + match change.tag() { + ChangeTag::Delete => removed += 1, + ChangeTag::Insert => added += 1, + ChangeTag::Equal => {} + } + } + + SnapshotStats { added, removed } +} pub fn display_snapshot_diff( snap: &Path, snap_new: &Path, short_path: &str, trace_path: Option<&Path>, -) { - println!(); - println!("{} {}", style("UPDATED").yellow().bold(), style(short_path).cyan()); + show_diff: bool, +) -> SnapshotStats { + let old_content = fs::read_to_string(snap).unwrap_or_default(); + let new_content = fs::read_to_string(snap_new).unwrap_or_default(); + + let old_stripped = strip_frontmatter(&old_content); + let new_stripped = strip_frontmatter(&new_content); + + let stats = count_diff_lines(old_stripped, new_stripped); + + print!("{} {}", style("UPDATED").yellow().bold(), style(short_path).cyan()); + println!( + " ({}, {})", + style(format!("+{}", stats.added)).green(), + style(format!("-{}", stats.removed)).red() + ); if let Some(trace) = trace_path { println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); } - println!(); + if show_diff { + println!(); + print_diff(old_stripped, new_stripped); + println!(); + } - let old_content = fs::read_to_string(snap).unwrap_or_default(); - let new_content = fs::read_to_string(snap_new).unwrap_or_default(); + stats +} - let old_stripped = strip_frontmatter(&old_content); +pub fn display_new_snapshot( + snap_new: &Path, + short_path: &str, + trace_path: Option<&Path>, + show_diff: bool, +) -> SnapshotStats { + let new_content = fs::read_to_string(snap_new).unwrap_or_default(); let new_stripped = strip_frontmatter(&new_content); + let line_count = new_stripped.lines().count(); - print_diff(old_stripped, new_stripped); -} - -pub fn display_new_snapshot(snap_new: &Path, short_path: &str, trace_path: Option<&Path>) { - println!("{} {}", style("CREATED").green().bold(), style(short_path).cyan()); + print!("{} {}", style("CREATED").green().bold(), style(short_path).cyan()); + println!(" ({})", style(format!("+{}", line_count)).green()); if let Some(trace) = trace_path { println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); } - println!(); + if show_diff { + println!(); + for (i, line) in new_stripped.lines().enumerate() { + println!("{} {}", style(format!("{:3}", i + 1)).dim(), line); + } + println!(); + } - let new_content = fs::read_to_string(snap_new).unwrap_or_default(); - for (i, line) in strip_frontmatter(&new_content).lines().enumerate() { - println!("{} {}", style(format!("{:3}", i + 1)).dim(), line); + SnapshotStats { added: line_count, removed: 0 } +} + +pub struct NextActionsArgs<'a> { + pub category_name: &'a str, + pub filters: &'a [String], + pub tests_passed: bool, + pub pending_count: usize, + pub total_lines_changed: usize, + pub trace_paths: &'a [PathBuf], + pub debug: bool, + pub showed_diffs: bool, +} + +pub fn print_next_actions(args: NextActionsArgs<'_>) { + let NextActionsArgs { + category_name, + filters, + tests_passed, + pending_count, + total_lines_changed, + trace_paths, + debug, + showed_diffs, + } = args; + + let ran_all = filters.is_empty(); + let filters_str = if ran_all { String::new() } else { format!(" {}", filters.join(" ")) }; + + let input = DecisionInput { + tests_passed, + pending_count, + total_lines_changed, + showed_diffs, + ran_all, + debug, + trace_count: trace_paths.len(), + }; + + match decision::decide_outcome(&input) { + Outcome::Clean => { + println!("{}", style("All tests passed, no pending snapshots.").green()); + } + Outcome::Failure(decision) => { + render_failure(&decision, category_name, &filters_str, trace_paths); + } + Outcome::Pending(decision) => { + render_pending( + &decision, + category_name, + &filters_str, + pending_count, + total_lines_changed, + ); + } } } -pub fn print_next_actions( +fn render_failure( + decision: &FailureDecision, category_name: &str, - filters: &[String], - tests_passed: bool, - pending_count: usize, + filters_str: &str, trace_paths: &[PathBuf], - debug: bool, ) { - let filters_str = - if filters.is_empty() { String::new() } else { format!(" {}", filters.join(" ")) }; + println!("{}", style("-".repeat(60)).dim()); + println!(); + println!("{}", style("Tests failed.").red()); - if tests_passed && pending_count == 0 { - println!("{}", style("No pending snapshots.").green()); - return; + if decision.show_debug_hint { + println!( + " Next: {}", + style(format!("just t {} --debug{}", category_name, filters_str)).cyan() + ); + } else if decision.show_trace_hint { + println!(" Next: consult trace files below"); } - if pending_count > 0 { - println!("{}", style("-".repeat(60)).dim()); - println!(); - println!("{} pending snapshot{}", pending_count, if pending_count == 1 { "" } else { "s" }); - println!(); - println!(" If this is expected, run {}", style("cargo insta accept").cyan()); - println!(" If this is not expected, run {}", style("cargo insta reject").cyan()); + if !trace_paths.is_empty() { println!(); + for trace in trace_paths.iter().take(decision.max_traces_to_show) { + println!(" {} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); + } + if trace_paths.len() > decision.max_traces_to_show { + let hidden = trace_paths.len() - decision.max_traces_to_show; + println!(" {}", style(format!("...and {} more trace file(s)", hidden)).dim()); + } } - if !tests_passed { - println!("{}", style("-".repeat(60)).dim()); + if let Some(count) = decision.pending_note { println!(); - println!("{}", style("Tests failed, consider consulting trace files.").red()); - if !debug { + println!( + " {}", + style(format!("Note: {} pending snapshot(s); review after fixing failures", count)) + .dim() + ); + } + + println!(); +} + +fn render_pending( + decision: &PendingDecision, + category_name: &str, + filters_str: &str, + pending_count: usize, + total_lines_changed: usize, +) { + println!("{}", style("-".repeat(60)).dim()); + println!(); + + let header = if decision.show_lines_changed { + format!("{} pending snapshot(s), {} lines changed", pending_count, total_lines_changed) + } else { + format!("{} pending snapshot{}", pending_count, if pending_count == 1 { "" } else { "s" }) + }; + println!("{}", header); + println!(); + + match decision.next_action { + NextAction::AcceptOrReject => { + let accept_cmd = format_accept_reject_cmd(category_name, filters_str, "accept"); + let reject_cmd = format_accept_reject_cmd(category_name, filters_str, "reject"); + println!(" Next: {}", style(&accept_cmd).cyan()); + println!(" Or: {}", style(&reject_cmd).cyan()); + } + NextAction::ReviewSubset => { + println!(" Next: {}", style(format!("just t {} NNN --diff", category_name)).cyan()); + println!(" {}", style("Review 1-3 tests at a time").dim()); + } + NextAction::ShowDiff => { println!( - "Enable debug tracing for more information: {}", - style(format!("just t {} --debug{}", category_name, filters_str)).cyan() + " Next: {}", + style(format!("just t {}{} --diff", category_name, filters_str)).cyan() ); } - if !trace_paths.is_empty() { - let maximum_shown = 10; - for trace in trace_paths.iter().take(maximum_shown) { - println!("{} {}", style("TRACE").magenta().bold(), style(trace.display()).cyan()); - } - if trace_paths.len() > maximum_shown { - let additional = trace_paths.len() - maximum_shown; - let additional = style(format!( - "An additional {additional} is stored in target/compiler-tracing" - )) - .dim(); - println!("{additional}"); - } - } - println!(); + } + + println!(); +} + +fn format_accept_reject_cmd(category_name: &str, filters_str: &str, action: &str) -> String { + if filters_str.is_empty() { + format!("just t {} {} --all", category_name, action) + } else { + format!("just t {} {}{}", category_name, action, filters_str) } } diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index f846b7d1..ef2f7142 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -161,7 +161,8 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { macro_rules! pos { ($id:expr) => {{ let cst = stabilized.ast_ptr($id).unwrap(); - let p = locate::offset_to_position(&content, cst.syntax_node_ptr().text_range().start()).unwrap(); + let range = cst.syntax_node_ptr().text_range(); + let p = locate::offset_to_position(&content, range.start()).unwrap(); format!("{}:{}", p.line, p.character) }}; } @@ -224,7 +225,8 @@ fn report_on_term( macro_rules! pos { ($id:expr) => {{ let cst = stabilized.ast_ptr($id).unwrap(); - let p = locate::offset_to_position(content, cst.syntax_node_ptr().text_range().start()).unwrap(); + let range = cst.syntax_node_ptr().text_range(); + let p = locate::offset_to_position(content, range.start()).unwrap(); format!("{}:{}", p.line, p.character) }}; } From 552bdca8d7653162e142a30ad8816dbe3a248e3e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 10:29:09 +0800 Subject: [PATCH 013/386] Tools for incremental testing and exclusion --- .claude/skills/compiler-scripts/SKILL.md | 69 ++++++++ .claude/skills/incremental-testing/SKILL.md | 50 ++++++ .claude/skills/type-checker-tests/SKILL.md | 160 ++++--------------- AGENTS.md | 1 + compiler-scripts/src/test_runner.rs | 1 + compiler-scripts/src/test_runner/category.rs | 7 + compiler-scripts/src/test_runner/cli.rs | 8 + compiler-scripts/src/test_runner/decision.rs | 24 ++- compiler-scripts/src/test_runner/nextest.rs | 2 + compiler-scripts/src/test_runner/pending.rs | 59 ++++++- compiler-scripts/src/test_runner/ui.rs | 18 ++- 11 files changed, 244 insertions(+), 155 deletions(-) create mode 100644 .claude/skills/compiler-scripts/SKILL.md create mode 100644 .claude/skills/incremental-testing/SKILL.md diff --git a/.claude/skills/compiler-scripts/SKILL.md b/.claude/skills/compiler-scripts/SKILL.md new file mode 100644 index 00000000..79742234 --- /dev/null +++ b/.claude/skills/compiler-scripts/SKILL.md @@ -0,0 +1,69 @@ +--- +name: compiler-scripts +description: Command reference for the compiler test runner CLI. Documents flags, options, and trace analysis. +--- + +# Compiler Scripts Command Reference + +CLI tools in `compiler-scripts/` for running integration tests. + +## Test Runner Commands + +### Run tests + +```bash +just t [filters...] # Run tests (summary output) +just t --diff [filters...] # Run with full inline diffs +just t --count 10 [filters...] # Show more snapshots (default: 3) +just t --debug [filters...] # Enable tracing +just t --verbose [filters...] # Show test progress +``` + +### Categories + +| Category | Alias | Description | +|----------|-------|-------------| +| checking | c | Type checker tests | +| lowering | l | Lowering tests | +| resolving | r | Resolver tests | +| lsp | - | LSP tests | + +### Snapshot commands + +```bash +just t accept [--all] [filters...] # Accept pending snapshots +just t reject [--all] [filters...] # Reject pending snapshots +``` + +Requires `--all` flag when no filters provided (safety guardrail). + +### Exclusion filters + +Hide snapshots ephemerally during a session: + +```bash +just t --exclude "pattern" # Single exclusion +just t --exclude "foo" --exclude "bar" # Multiple exclusions +EXCLUDE_SNAPSHOTS="foo,bar" just t # Via environment variable +``` + +### Filters + +Space-delimited, passed through to nextest. Mix numbers and patterns: + +```bash +just t c 101 102 # Run tests 101 and 102 +just t c pattern # Filter by name pattern +just t c 101 102 pattern # Numbers + pattern together +``` + +## Debugging Traces + +When `--debug` is used, traces are written to `target/compiler-tracing/{test_id}_{module}.jsonl`. + +```bash +wc -l target/compiler-tracing/*.jsonl # Check sizes +shuf -n 20 target/compiler-tracing/NNN_*.jsonl | jq . # Sample +jq 'select(.level == "DEBUG")' file.jsonl # Filter by level +jq 'select(.target | contains("unification"))' file.jsonl +``` diff --git a/.claude/skills/incremental-testing/SKILL.md b/.claude/skills/incremental-testing/SKILL.md new file mode 100644 index 00000000..ebdb953e --- /dev/null +++ b/.claude/skills/incremental-testing/SKILL.md @@ -0,0 +1,50 @@ +--- +name: incremental-testing +description: SINGLE SOURCE OF TRUTH for running compiler integration tests + snapshots. Use for checking/lowering/resolving/lsp. Follow exactly; do not discover tests via filesystem. +--- + +# Compiler Integration Testing (AUTHORITATIVE) + +Make sure to load the compiler-scripts skill for command syntax and flags. + +## NON-NEGOTIABLE RULES (must follow) + +1. You MUST run compiler integration tests ONLY via: + `just t [filters...]` + +2. You MUST NOT perform test discovery by inspecting the filesystem. + Specifically, you MUST NOT `glob`, `find`, or `rg` under: + `tests-integration/fixtures/**` + +3. Filters MUST come ONLY from: + - The user's explicit test id/name/pattern, OR + - The CLI output from a previous `just t ...` run. + Filters MUST NOT come from fixture directory exploration. + +4. STOP CONDITION: If you already know `` and `[filters...]`, + run the `just t ...` command now. Do not gather more context. + +## Decision procedure (do exactly this) + +1. Identify category: one of `c` (checking), `l` (lowering), `r` (resolving), `lsp`. +2. If the user provided any ids/patterns → pass them as space-delimited filters. +3. Else → run the category with no filters. +4. If results are too broad → rerun using ids/patterns copied from CLI output. + +## Quick Examples + +```bash +just t c # Run all checking tests +just t c 101 # Run test 101 in checking +just t c 101 102 pattern # Multiple filters +just t c accept --all # Accept all pending checking snapshots +``` + +## Forbidden (never do this) + +- `rg tests-integration/fixtures` +- `find tests-integration/fixtures` +- `glob tests-integration/fixtures/**` +- "Open fixture files to see what tests exist" +- "List fixture directories to decide which test to run" +- Any form of filesystem exploration to discover tests diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index e0ce0822..4e906761 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -6,26 +6,9 @@ allowed-tools: Bash(mkdir:*) # Type Checker Integration Tests -Use this skill when adding new type checker functions or expanding behavior. +Make sure to load the compiler-scripts skill for command syntax and flags. The category is `checking`. -**Language:** Test fixtures use PureScript syntax, not Haskell. - -## Quick Reference - -| Action | Command | -|--------|---------| -| Find next test number | `ls tests-integration/fixtures/checking/ \| tail -5` | -| Run a test or multiple tests | `just t checking NNN` or `just t c 101 102` | -| Run with full diffs | `just t checking NNN --diff` | -| Run with tracing enabled | `just t checking --debug NNN` | -| Run all checking tests | `just t checking` or `just t c` | -| Accept all pending snapshots | `cargo insta accept` | - -Use `just t checking --help` for all options. - -**Output modes:** -- Default: Shows summary only (snapshot paths + line counts like `+5, -3`) -- `--diff`: Shows full inline diffs for each snapshot +**Language:** Fixtures use PureScript syntax, not Haskell. ## Creating a Test @@ -35,7 +18,9 @@ Use `just t checking --help` for all options. mkdir tests-integration/fixtures/checking/{NNN_descriptive_name} ``` -Tests are auto-discovered by `build.rs` - no manual registration needed. +Find next number: `ls tests-integration/fixtures/checking/ | tail -5` + +Tests are auto-discovered by `build.rs`. ### 2. Write Main.purs @@ -54,31 +39,22 @@ test' [x] = x **Guidelines:** - Test ONE specific behavior per fixture -- Name tests descriptively: `test`, `test'`, `test2`, `test2'`, etc. -- Include edge cases relevant to the behavior being tested +- Name tests: `test`, `test'`, `test2`, `test2'`, etc. +- Include edge cases relevant to the behavior -### 3. Generate and review snapshot +### 3. Run and review ```bash -just t checking NNN # summary only -just t checking NNN --diff # full diff output +just t checking NNN MMM ``` -This outputs: -- `CREATED path (+N)` (green) showing new snapshot with line count -- `UPDATED path (+N, -M)` (yellow) showing changed lines count - -Use `--diff` to see full inline diffs when reviewing changes. - ## Multi-File Tests -For testing imports, re-exports, or cross-module behavior, add multiple `.purs` files -to the same fixture directory. The type checker loads all `.purs` files in the folder. +For imports, re-exports, or cross-module behavior: -**Example structure:** ``` tests-integration/fixtures/checking/NNN_import_test/ -├── Main.purs # The test file (snapshot generated for Main) +├── Main.purs # Test file (snapshot generated) ├── Lib.purs # Supporting module └── Main.snap # Generated snapshot ``` @@ -103,14 +79,10 @@ test :: Maybe Int test = Just life ``` -**Key points:** -- Module name must match filename (`Lib.purs` -> `module Lib where`) -- Only `Main.purs` generates a snapshot (the test runs against `Main`) -- Use standard PureScript import syntax - -## Reviewing Snapshots +- Module name must match filename +- Only `Main.purs` generates a snapshot -Snapshots have this structure: +## Snapshot Structure ``` Terms @@ -125,101 +97,31 @@ Errors ErrorKind { details } at [location] ``` -### Acceptance Criteria +## Acceptance Criteria -**Before accepting, verify:** +Before accepting, verify: -1. **Types are correct** - Check that inferred types match expectations - - `test :: Array Int -> Int` - explicit signature preserved - - `test' :: forall t. Array t -> t` - polymorphism inferred correctly +1. **Types are correct** + - `test :: Array Int -> Int` - signature preserved + - `test' :: forall t. Array t -> t` - polymorphism inferred -2. **No unexpected `???`** - This indicates inference failure - - `test :: ???` - STOP: the term failed to type check - - `CannotUnify { ??? -> ???, Int }` - OK in error tests, shows unresolved unification variables +2. **No unexpected `???`** + - `test :: ???` - STOP: inference failure + - `CannotUnify { ??? -> ???, Int }` - OK in error tests -3. **Errors appear where expected** - For tests validating error behavior - - Confirm error kind matches expectations (e.g., `NoInstanceFound`, `CannotUnify`) - - Verify error location points to the correct declaration +3. **Errors appear where expected** + - Confirm error kind matches (`NoInstanceFound`, `CannotUnify`) + - Verify location points to correct declaration 4. **Polymorphism is appropriate** - - Check type variable names (`t6`, `a`, etc.) are scoped correctly - - Verify constraints propagate as expected + - Type variables scoped correctly + - Constraints propagate as expected -### Common Issues +## Common Issues | Symptom | Likely Cause | |---------|--------------| -| `test :: ???` | Test code has syntax error or uses undefined names | -| Unexpected monomorphism | Missing polymorphic context or over-constrained signature | -| Wrong error location | Check binder/expression placement in source | +| `test :: ???` | Syntax error or undefined names | +| Unexpected monomorphism | Missing polymorphic context | +| Wrong error location | Check binder/expression placement | | Missing types in snapshot | Module header or imports incorrect | - -## Accept and Verify - -```bash -# Accept only after thorough review -cargo insta accept - -# Verify all checking tests pass -just t checking -``` - -## Debugging - -When investigating a potential compiler bug: - -```bash -# Focus on single test to reduce noise -just t checking NNN - -# Enable tracing to see type checker behaviour -just t checking --debug NNN -``` - -### Trace Files - -The `--debug` flag emits detailed type checker traces to `target/compiler-tracing/`. - -**Trace file naming:** `{test_id}_{module_name}.jsonl` -- Example: `200_int_compare_transitive_Main.jsonl` - -**Output format:** JSON Lines (one JSON object per line), containing: -- `timestamp` - when the event occurred -- `level` - DEBUG, INFO, or TRACE -- `fields` - trace data (e.g., types being unified) -- `target` - the module emitting the trace (e.g., `checking::algorithm::unification`) -- `span`/`spans` - current span and span stack - -**Example trace line:** -```json -{"timestamp":"...","level":"DEBUG","fields":{"t1":"?0","t2":"Int"},"target":"checking::algorithm::unification","span":{"name":"unify"}} -``` - -When `--debug` is used, the trace file path is shown alongside pending snapshots: -``` -UPDATED tests-integration/fixtures/checking/200_int_compare_transitive/Main.snap - TRACE target/compiler-tracing/200_int_compare_transitive_Main.jsonl -``` - -### Analysing Traces - -Trace files can be large for complex tests. Use sampling and filtering: - -```bash -# Check file size and line count -wc -l target/compiler-tracing/NNN_*.jsonl - -# Sample random lines to get an overview -shuf -n 20 target/compiler-tracing/NNN_*.jsonl | jq . - -# Filter by level -jq 'select(.level == "DEBUG")' target/compiler-tracing/NNN_*.jsonl - -# Filter by target module -jq 'select(.target | contains("unification"))' target/compiler-tracing/NNN_*.jsonl - -# Extract specific fields -jq '{level, target, fields}' target/compiler-tracing/NNN_*.jsonl -``` - -You should run `just t checking` to check for regressions. diff --git a/AGENTS.md b/AGENTS.md index 325212f8..e31e6a4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,4 +30,5 @@ PureScript compiler frontend in Rust using rowan (lossless syntax trees) and que ## Skills +Load `.claude/skills/compiler-scripts` when running integration tests. Load `.claude/skills/type-checker-tests` when implementing type checker tests. diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs index b1166089..356e805e 100644 --- a/compiler-scripts/src/test_runner.rs +++ b/compiler-scripts/src/test_runner.rs @@ -43,6 +43,7 @@ pub fn run_category(category: TestCategory, args: &RunArgs) -> TestOutcome { filters: &args.filters, tests_passed, pending_count: pending_result.count, + excluded_count: pending_result.excluded_count, total_lines_changed: pending_result.total_lines_changed, trace_paths: &trace_paths, debug: args.debug, diff --git a/compiler-scripts/src/test_runner/category.rs b/compiler-scripts/src/test_runner/category.rs index 10071d3e..c26cc949 100644 --- a/compiler-scripts/src/test_runner/category.rs +++ b/compiler-scripts/src/test_runner/category.rs @@ -23,6 +23,13 @@ impl TestCategory { format!("tests-integration/fixtures/{}", self.as_str()) } + pub fn snapshot_path_fragments(&self) -> Vec { + vec![ + format!("tests-integration/fixtures/{}", self.as_str()), + format!("tests-integration/tests/snapshots/{}__", self.as_str()), + ] + } + pub fn extra_env(&self, debug: bool) -> Vec<(&'static str, String)> { if debug { vec![("TRACE_LEVEL", "debug".to_string())] } else { vec![] } } diff --git a/compiler-scripts/src/test_runner/cli.rs b/compiler-scripts/src/test_runner/cli.rs index f0aabd4c..a3633391 100644 --- a/compiler-scripts/src/test_runner/cli.rs +++ b/compiler-scripts/src/test_runner/cli.rs @@ -40,4 +40,12 @@ pub struct RunArgs { /// Show full diffs (by default only shows summary to reduce output) #[arg(long)] pub diff: bool, + + /// Maximum number of snapshots to show (default: 3) + #[arg(long, default_value = "3")] + pub count: usize, + + /// Exclude snapshots matching pattern (substring match, repeatable) + #[arg(long)] + pub exclude: Vec, } diff --git a/compiler-scripts/src/test_runner/decision.rs b/compiler-scripts/src/test_runner/decision.rs index 1713a96d..78ba6540 100644 --- a/compiler-scripts/src/test_runner/decision.rs +++ b/compiler-scripts/src/test_runner/decision.rs @@ -9,6 +9,7 @@ pub struct DecisionInput { pub ran_all: bool, pub debug: bool, pub trace_count: usize, + pub max_count: usize, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -78,15 +79,7 @@ pub fn decide_outcome(input: &DecisionInput) -> Outcome { } pub fn decide_snapshot_limits(input: &DecisionInput) -> SnapshotDisplayLimits { - let many_pending = input.ran_all && input.pending_count > 3; - - let max_shown = if input.showed_diffs { - usize::MAX - } else if many_pending { - 2 - } else { - 5 - }; + let max_shown = if input.showed_diffs { usize::MAX } else { input.max_count }; SnapshotDisplayLimits { max_shown } } @@ -104,6 +97,7 @@ mod tests { ran_all: true, debug: false, trace_count: 0, + max_count: 3, } } @@ -187,17 +181,17 @@ mod tests { } #[test] - fn snapshot_limits_2_when_many_pending() { - let input = DecisionInput { pending_count: 10, ran_all: true, ..base_input() }; + fn snapshot_limits_uses_max_count() { + let input = DecisionInput { max_count: 10, ..base_input() }; let limits = decide_snapshot_limits(&input); - assert_eq!(limits.max_shown, 2); + assert_eq!(limits.max_shown, 10); } #[test] - fn snapshot_limits_5_default() { - let input = DecisionInput { pending_count: 2, ..base_input() }; + fn snapshot_limits_default_count() { + let input = base_input(); let limits = decide_snapshot_limits(&input); - assert_eq!(limits.max_shown, 5); + assert_eq!(limits.max_shown, 3); } #[test] diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs index d3c4e0e7..8f4b81d0 100644 --- a/compiler-scripts/src/test_runner/nextest.rs +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -65,6 +65,8 @@ pub fn run_nextest( verbose: true, debug: args.debug, diff: args.diff, + count: args.count, + exclude: args.exclude.clone(), }; let mut retry = build_nextest_command(category, &verbose_args, fixture_hashes); let _ = retry.status(); diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs index 6cd6bdef..846ec45d 100644 --- a/compiler-scripts/src/test_runner/pending.rs +++ b/compiler-scripts/src/test_runner/pending.rs @@ -19,6 +19,7 @@ struct PendingSnapshotJson { pub struct PendingResult { pub count: usize, + pub excluded_count: usize, pub total_lines_changed: usize, } @@ -44,7 +45,7 @@ pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> let pending = pending.trim(); let cwd = env::current_dir().unwrap(); - let fixtures_fragment = category.fixtures_subdir_fragment(); + let path_fragments = category.snapshot_path_fragments(); let mut snapshots = Vec::new(); @@ -61,7 +62,7 @@ pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> continue; }; - if !snapshot_path.contains(&fixtures_fragment) { + if !path_fragments.iter().any(|f| snapshot_path.contains(f)) { continue; } @@ -90,6 +91,36 @@ pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> snapshots } +fn collect_exclusion_patterns(args: &RunArgs) -> Vec { + let mut patterns = args.exclude.clone(); + + if let Ok(env_patterns) = env::var("EXCLUDE_SNAPSHOTS") { + for pattern in env_patterns.split(',') { + let pattern = pattern.trim(); + if !pattern.is_empty() { + patterns.push(pattern.to_string()); + } + } + } + + patterns +} + +fn apply_exclusions( + snapshots: Vec, + patterns: &[String], +) -> (Vec, usize) { + if patterns.is_empty() { + return (snapshots, 0); + } + + let (excluded, visible): (Vec<_>, Vec<_>) = snapshots + .into_iter() + .partition(|info| patterns.iter().any(|p| info.short_path.contains(p))); + + (visible, excluded.len()) +} + pub fn process_pending_snapshots( category: TestCategory, args: &RunArgs, @@ -102,22 +133,34 @@ pub fn process_pending_snapshots( info.trace_path = category.trace_for_snapshot(Path::new(&info.snapshot_path), trace_paths); } - let count = snapshots.len(); + // Apply exclusion filters + let exclusion_patterns = collect_exclusion_patterns(args); + let (visible, excluded_count) = apply_exclusions(snapshots, &exclusion_patterns); + + if excluded_count > 0 { + println!( + "{}", + style(format!("info: excluded {} snapshot(s) by pattern", excluded_count)).dim() + ); + } + + let pending_count = visible.len(); let mut total_lines_changed = 0; let limits = decision::decide_snapshot_limits(&DecisionInput { tests_passed: true, // not relevant for snapshot limits - pending_count: count, + pending_count, total_lines_changed: 0, // not known yet, not relevant for limits showed_diffs: args.diff, ran_all: args.filters.is_empty(), debug: args.debug, trace_count: trace_paths.len(), + max_count: args.count, }); let max_shown = limits.max_shown; - for info in snapshots.iter().take(max_shown) { + for info in visible.iter().take(max_shown) { let snap = Path::new(&info.snapshot_path); let stats = if info.is_update { ui::display_snapshot_diff( @@ -138,15 +181,15 @@ pub fn process_pending_snapshots( total_lines_changed += stats.added + stats.removed; } - if count > max_shown { - let hidden = count - max_shown; + if pending_count > max_shown { + let hidden = pending_count - max_shown; println!( "{}", style(format!("...and {} more pending snapshot(s) not shown", hidden)).dim() ); } - PendingResult { count, total_lines_changed } + PendingResult { count: visible.len(), excluded_count, total_lines_changed } } pub struct AcceptRejectResult { diff --git a/compiler-scripts/src/test_runner/ui.rs b/compiler-scripts/src/test_runner/ui.rs index a4a44397..74a3c56d 100644 --- a/compiler-scripts/src/test_runner/ui.rs +++ b/compiler-scripts/src/test_runner/ui.rs @@ -99,6 +99,7 @@ pub struct NextActionsArgs<'a> { pub filters: &'a [String], pub tests_passed: bool, pub pending_count: usize, + pub excluded_count: usize, pub total_lines_changed: usize, pub trace_paths: &'a [PathBuf], pub debug: bool, @@ -111,6 +112,7 @@ pub fn print_next_actions(args: NextActionsArgs<'_>) { filters, tests_passed, pending_count, + excluded_count, total_lines_changed, trace_paths, debug, @@ -128,6 +130,7 @@ pub fn print_next_actions(args: NextActionsArgs<'_>) { ran_all, debug, trace_count: trace_paths.len(), + max_count: 3, // not used for outcome decisions }; match decision::decide_outcome(&input) { @@ -143,6 +146,7 @@ pub fn print_next_actions(args: NextActionsArgs<'_>) { category_name, &filters_str, pending_count, + excluded_count, total_lines_changed, ); } @@ -196,6 +200,7 @@ fn render_pending( category_name: &str, filters_str: &str, pending_count: usize, + excluded_count: usize, total_lines_changed: usize, ) { println!("{}", style("-".repeat(60)).dim()); @@ -203,6 +208,13 @@ fn render_pending( let header = if decision.show_lines_changed { format!("{} pending snapshot(s), {} lines changed", pending_count, total_lines_changed) + } else if excluded_count > 0 { + format!( + "{} pending snapshot{} ({} excluded)", + pending_count, + if pending_count == 1 { "" } else { "s" }, + excluded_count + ) } else { format!("{} pending snapshot{}", pending_count, if pending_count == 1 { "" } else { "s" }) }; @@ -213,12 +225,12 @@ fn render_pending( NextAction::AcceptOrReject => { let accept_cmd = format_accept_reject_cmd(category_name, filters_str, "accept"); let reject_cmd = format_accept_reject_cmd(category_name, filters_str, "reject"); - println!(" Next: {}", style(&accept_cmd).cyan()); - println!(" Or: {}", style(&reject_cmd).cyan()); + println!(" Next: {}", style(&accept_cmd).green()); + println!(" Or: {}", style(&reject_cmd).red()); } NextAction::ReviewSubset => { println!(" Next: {}", style(format!("just t {} NNN --diff", category_name)).cyan()); - println!(" {}", style("Review 1-3 tests at a time").dim()); + println!(" {}", style("Hint: Review 1-2 tests at a time").dim()); } NextAction::ShowDiff => { println!( From 2320d3f48a292493e1730c095d60b2c9b3b69bf3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 11:43:29 +0800 Subject: [PATCH 014/386] Add error steps for do_bind and do_discard --- compiler-core/checking/src/algorithm/term.rs | 34 ++++++++++++++++++++ compiler-core/checking/src/error.rs | 3 ++ compiler-core/checking/src/trace.rs | 3 ++ compiler-core/diagnostics/src/context.rs | 3 ++ 4 files changed, 43 insertions(+) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 3cfb4b2a..c12f8e13 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -606,6 +606,7 @@ where Ok(inferred_type) } +#[tracing::instrument(skip_all, name = "infer_do")] fn infer_do( state: &mut CheckState, context: &CheckContext, @@ -1098,6 +1099,7 @@ where ) } +#[tracing::instrument(skip_all, name = "infer_do_bind")] fn infer_do_bind( state: &mut CheckState, context: &CheckContext, @@ -1113,6 +1115,22 @@ where return Ok(context.prim.unknown); }; + state.with_error_step(ErrorStep::InferringDoBind(expression), |state| { + infer_do_bind_core(state, context, bind_type, accumulated_type, expression, binder_type) + }) +} + +fn infer_do_bind_core( + state: &mut CheckState, + context: &CheckContext, + bind_type: TypeId, + accumulated_type: TypeId, + expression: lowering::ExpressionId, + binder_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ // expression_type := m a let expression_type = infer_expression(state, context, expression)?; // lambda_type := a -> m b @@ -1146,6 +1164,7 @@ where ) } +#[tracing::instrument(skip_all, name = "infer_do_discard")] fn infer_do_discard( state: &mut CheckState, context: &CheckContext, @@ -1160,6 +1179,21 @@ where return Ok(context.prim.unknown); }; + state.with_error_step(ErrorStep::InferringDoDiscard(expression), |state| { + infer_do_discard_core(state, context, discard_type, accumulated_type, expression) + }) +} + +fn infer_do_discard_core( + state: &mut CheckState, + context: &CheckContext, + discard_type: TypeId, + accumulated_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ // expression_type := m a let expression_type = infer_expression(state, context, expression)?; diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index f55fd050..7525a1d8 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -22,6 +22,9 @@ pub enum ErrorStep { InferringExpression(lowering::ExpressionId), CheckingExpression(lowering::ExpressionId), + + InferringDoBind(lowering::ExpressionId), + InferringDoDiscard(lowering::ExpressionId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/compiler-core/checking/src/trace.rs b/compiler-core/checking/src/trace.rs index a452e84f..44247a3c 100644 --- a/compiler-core/checking/src/trace.rs +++ b/compiler-core/checking/src/trace.rs @@ -32,6 +32,9 @@ where ErrorStep::TypeDeclaration(id) => { context.indexed.type_item_ptr(&context.stabilized, *id).next()? } + ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { + context.stabilized.syntax_ptr(*id)? + } }; let range = pointer.text_range(); diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index 8eb3172e..4dee6eae 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -73,6 +73,9 @@ impl<'a> DiagnosticsContext<'a> { ErrorStep::TypeDeclaration(id) => { self.indexed.type_item_ptr(self.stabilized, *id).next()? } + ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { + self.stabilized.syntax_ptr(*id)? + } }; self.span_from_syntax_ptr(&ptr) } From 00ca279c7b96abff83e224007621b86ea5fcd60d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 11:47:41 +0800 Subject: [PATCH 015/386] Use significant range extraction algorithm This introduces the significant_ranges function in diagnostics::context to extract the significant ranges in a node. Annotations are attached to InstanceDeclaration, not InstanceChain; this makes it impossible for the naive Annotation-stripping approach to work, leading to confusing offsets during diagnostic reporting. --- compiler-core/diagnostics/src/context.rs | 63 ++++++++++++++++--- .../Main.snap | 7 ++- .../123_incomplete_instance_head/Main.snap | 3 +- .../Main.snap | 3 +- .../Main.snap | 5 +- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index 4dee6eae..4d028a78 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -2,11 +2,63 @@ use checking::CheckedModule; use checking::error::ErrorStep; use indexing::IndexedModule; use rowan::ast::{AstNode, AstPtr}; +use rowan::{NodeOrToken, TextRange}; use stabilizing::StabilizedModule; -use syntax::{SyntaxKind, SyntaxNode, SyntaxNodePtr}; +use syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxNodePtr}; use crate::Span; +fn is_trivia(element: &SyntaxElement) -> bool { + match element { + NodeOrToken::Node(node) => matches!(node.kind(), SyntaxKind::Annotation), + NodeOrToken::Token(token) => { + token.text_range().is_empty() + || matches!(token.kind(), SyntaxKind::LAYOUT_SEPARATOR | SyntaxKind::ELSE) + } + } +} + +fn first_significant_range(node: &SyntaxNode) -> Option { + for child in node.children_with_tokens() { + if is_trivia(&child) { + continue; + } + match child { + NodeOrToken::Token(token) => return Some(token.text_range()), + NodeOrToken::Node(node) => { + if let Some(range) = first_significant_range(&node) { + return Some(range); + } + } + } + } + None +} + +fn last_significant_range(node: &SyntaxNode) -> Option { + let mut significant = None; + for child in node.children_with_tokens() { + if is_trivia(&child) { + continue; + } + match child { + NodeOrToken::Token(token) => significant = Some(token.text_range()), + NodeOrToken::Node(node) => { + if let Some(range) = last_significant_range(&node) { + significant = Some(range); + } + } + } + } + significant +} + +fn significant_ranges(node: &SyntaxNode) -> Option { + let start = first_significant_range(node)?; + let end = last_significant_range(node)?; + Some(start.cover(end)) +} + pub struct DiagnosticsContext<'a> { pub content: &'a str, pub root: &'a SyntaxNode, @@ -40,14 +92,7 @@ impl<'a> DiagnosticsContext<'a> { } fn span_from_syntax_node(&self, node: &SyntaxNode) -> Option { - let mut children = node.children_with_tokens().peekable(); - - children.next_if(|child| matches!(child.kind(), SyntaxKind::Annotation)); - - let start = children.peek().map(|child| child.text_range()); - let end = children.last().map(|child| child.text_range()); - - let range = start.zip(end).map(|(start, end)| start.cover(end))?; + let range = significant_ranges(node)?; Some(Span::new(range.start().into(), range.end().into())) } diff --git a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap index 56a3fa83..704f693c 100644 --- a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap +++ b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -16,6 +17,6 @@ instance Show (Int :: Type) chain: 0 Diagnostics -error[CannotUnify] at 59..118: Cannot unify 'Int' with 'String' -error[CannotUnify] at 59..118: Cannot unify 'Int -> Int' with 'Int -> String' -error[InstanceMemberTypeMismatch] at 59..118: Instance member type mismatch: expected 'Int -> String', got 'Int -> Int' +error[CannotUnify] at 61..118: Cannot unify 'Int' with 'String' +error[CannotUnify] at 61..118: Cannot unify 'Int -> Int' with 'Int -> String' +error[InstanceMemberTypeMismatch] at 61..118: Instance member type mismatch: expected 'Int -> String', got 'Int -> Int' diff --git a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap index c49393bc..72bdf9b8 100644 --- a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap +++ b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,4 +22,4 @@ instance Pair (Int :: Type) chain: 0 Diagnostics -error[InstanceHeadMismatch] at 138..191: Instance head mismatch: expected 2 arguments, got 1 +error[InstanceHeadMismatch] at 140..191: Instance head mismatch: expected 2 arguments, got 1 diff --git a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap index fc4e2efb..e3e88525 100644 --- a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap +++ b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -35,4 +36,4 @@ instance Functor (Box :: Type -> Type) chain: 0 Diagnostics -error[NoInstanceFound] at 146..240: No instance found for: Show (~&1 :: Type) +error[NoInstanceFound] at 148..240: No instance found for: Show (~&1 :: Type) diff --git a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap index 7a8565c3..ba893cfe 100644 --- a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap +++ b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -16,5 +17,5 @@ instance Show (Boolean :: Type) chain: 0 Diagnostics -error[CannotUnify] at 59..138: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' -error[InstanceMemberTypeMismatch] at 59..138: Instance member type mismatch: expected 'Boolean -> String', got 'forall (a :: Type). (a :: Type) -> String' +error[CannotUnify] at 61..138: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' +error[InstanceMemberTypeMismatch] at 61..138: Instance member type mismatch: expected 'Boolean -> String', got 'forall (a :: Type). (a :: Type) -> String' From 29bd7aca2538c6861c2d410a2d1c2892e8a79c8e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 11:48:09 +0800 Subject: [PATCH 016/386] Add basic test for do bind error tracking --- .../checking/215_do_bind_error/Main.purs | 15 ++++++++++++ .../checking/215_do_bind_error/Main.snap | 24 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 3 files changed, 41 insertions(+) create mode 100644 tests-integration/fixtures/checking/215_do_bind_error/Main.purs create mode 100644 tests-integration/fixtures/checking/215_do_bind_error/Main.snap diff --git a/tests-integration/fixtures/checking/215_do_bind_error/Main.purs b/tests-integration/fixtures/checking/215_do_bind_error/Main.purs new file mode 100644 index 00000000..441806c9 --- /dev/null +++ b/tests-integration/fixtures/checking/215_do_bind_error/Main.purs @@ -0,0 +1,15 @@ +module Main where + +foreign import data Effect :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import add :: Int -> Int -> Int + +test :: Effect Int +test = do + a <- pure 123456 + b <- pure "life" + pure (add a b) + diff --git a/tests-integration/fixtures/checking/215_do_bind_error/Main.snap b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap new file mode 100644 index 00000000..7dfe42e7 --- /dev/null +++ b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +add :: Int -> Int -> Int +test :: Effect Int + +Types +Effect :: Type -> Type + +Roles +Effect = [Nominal] + +Diagnostics +error[CannotUnify] at 359..370: Cannot unify 'String' with 'Int' diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 3c94e8ec..a89bcdb7 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -455,3 +455,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_213_row_constraint_multiple_main() { run_test("213_row_constraint_multiple", "Main"); } #[rustfmt::skip] #[test] fn test_214_row_nub_left_bias_main() { run_test("214_row_nub_left_bias", "Main"); } + +#[rustfmt::skip] #[test] fn test_215_do_bind_error_main() { run_test("215_do_bind_error", "Main"); } From 9e06bbaa3050e1b79b885c966ee56eba20c6d497 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 20:03:16 +0800 Subject: [PATCH 017/386] Improve inference for do statements This improves inference for do statements by using a unification variable as a stand-in for the innermost expression when emulating desugard bind/discard expressions. Inferring the pure_expression too early causes the unification variables we created for the do statement binders to be solved too eagerly; in the case of 215, the error becomes associated to `pure "life"` rather than `add a b`. --- compiler-core/checking/src/algorithm/term.rs | 125 +++++++++++------- .../checking/053_do_polymorphic/Main.snap | 3 +- .../checking/117_do_ado_constrained/Main.snap | 11 +- .../checking/215_do_bind_error/Main.snap | 2 +- 4 files changed, 87 insertions(+), 54 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index c12f8e13..03e66c15 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -652,50 +652,81 @@ where return Ok(context.prim.unknown); }; - // With the binders and let-bound names in scope, infer - // the type of the last expression as our starting point. + // Create a fresh unification variable for the pure_expression. + // Inferring pure_expression directly may solve the unification + // variables introduced in the first pass. This is undesirable, + // because the errors would be attributed incorrectly to the + // do statements rather than the pure_expression itself. // - // main = do - // pure 42 - // y <- pure "Hello!" - // pure $ Message y + // main = do + // pure 42 + // y <- pure "Hello!" + // pure $ Message y // - // accumulated_type := Effect Message - let mut accumulated_type = infer_expression(state, context, *pure_expression)?; - - // Then, infer do statements in reverse order to emulate - // inside-out type inference for desugared do statements. + // pure_expression :: Effect Message + // pure_expression_type := ?pure_expression + let pure_expression_type = state.fresh_unification_type(context); + + // The desugard form of a do-expression is checked inside out. An + // expression like `bind m_a (\a -> m_b)` is checked by inferring + // the type of the continuation before the `bind` application can + // be checked. In the example we have above, we have the following: + // + // bind (pure "Hello!") (\y -> pure $ Message y) + // + // discard (pure 42) (\_ -> ) + // + // To emulate this, the infer_do_bind rule applies `bind` to the + // inferred expression type and a function type synthesised using + // the unification variables we created previously. + // + // First, the bind statement: + // + // expression_type := Effect String + // + // lambda_type := ?y -> continuation_type + // := ?y -> ?pure_expression + // + // bind_type := m a -> (a -> m b) -> m b + // apply(expression) := (String -> Effect ?b) -> Effect ?b + // apply(lambda) := Effect ?b + // >> + // >> ?y := String + // >> ?pure_expression := Effect ?b + // + // continuation_type := Effect ?b + // + // Then, the discard statement: + // + // expression_type := Effect Int + // + // discard_type := m a -> (a -> m b) -> m b + // apply(expression) := (Int -> Effect ?b) -> Effect ?b + // extract(lambda) := Effect ?b + // + // continuation_type := Effect ?b + let mut continuation_type = pure_expression_type; for (binder, expression) in bind_statements.iter().rev() { - accumulated_type = if let Some(binder) = binder { - // This applies bind_type to expression_type to get - // bind_applied, which is then applied to lambda_type - // to get the accumulated_type and to solve ?y. - // - // bind_type := m a -> (a -> m b) -> m b - // expression_type := Effect String - // - // bind_applied := (String -> Effect b) -> Effect b - // lambda_type := ?y -> Effect Message - // - // accumulated_type := Effect Message - infer_do_bind(state, context, bind_type, accumulated_type, *expression, *binder)? + continuation_type = if let Some(binder) = binder { + infer_do_bind(state, context, bind_type, continuation_type, *expression, *binder)? } else { - // This applies discard_type to expression_type to - // get discard_applied, which is then deconstructed - // to subsume against the `Effect b`. - // - // discard_type := m a -> (a -> m b) -> m b - // expression_type := Effect Int - // - // discard_applied := (Int -> Effect b) -> Effect b - // accumulated_type := Effect Message - // - // accumulated_type <: Effect b - infer_do_discard(state, context, discard_type, accumulated_type, *expression)? - } + infer_do_discard(state, context, discard_type, continuation_type, *expression)? + }; } - Ok(accumulated_type) + // Finally, check the pure expression against the pure_expression_type. + // At this point we're confident that the unification variables created + // for the do statement bindings have been solved to concrete types. + // By performing a check against a unification variable instead of + // performing inference on pure_expression early, we get better errors. + // + // pure_expression :: Effect Message + // continuation_type := Effect ?b + // >> + // >> ?b := Message + let _ = check_expression(state, context, *pure_expression, pure_expression_type)?; + + Ok(continuation_type) } fn infer_ado( @@ -1104,7 +1135,7 @@ fn infer_do_bind( state: &mut CheckState, context: &CheckContext, bind_type: TypeId, - accumulated_type: TypeId, + continuation_type: TypeId, expression: Option, binder_type: TypeId, ) -> QueryResult @@ -1116,7 +1147,7 @@ where }; state.with_error_step(ErrorStep::InferringDoBind(expression), |state| { - infer_do_bind_core(state, context, bind_type, accumulated_type, expression, binder_type) + infer_do_bind_core(state, context, bind_type, continuation_type, expression, binder_type) }) } @@ -1124,7 +1155,7 @@ fn infer_do_bind_core( state: &mut CheckState, context: &CheckContext, bind_type: TypeId, - accumulated_type: TypeId, + continuation_type: TypeId, expression: lowering::ExpressionId, binder_type: TypeId, ) -> QueryResult @@ -1134,7 +1165,7 @@ where // expression_type := m a let expression_type = infer_expression(state, context, expression)?; // lambda_type := a -> m b - let lambda_type = state.make_function(&[binder_type], accumulated_type); + let lambda_type = state.make_function(&[binder_type], continuation_type); // bind_type := m a -> (a -> m b) -> m b // expression_type := m a @@ -1169,7 +1200,7 @@ fn infer_do_discard( state: &mut CheckState, context: &CheckContext, discard_type: TypeId, - accumulated_type: TypeId, + continuation_type: TypeId, expression: Option, ) -> QueryResult where @@ -1180,7 +1211,7 @@ where }; state.with_error_step(ErrorStep::InferringDoDiscard(expression), |state| { - infer_do_discard_core(state, context, discard_type, accumulated_type, expression) + infer_do_discard_core(state, context, discard_type, continuation_type, expression) }) } @@ -1188,7 +1219,7 @@ fn infer_do_discard_core( state: &mut CheckState, context: &CheckContext, discard_type: TypeId, - accumulated_type: TypeId, + continuation_type: TypeId, expression: lowering::ExpressionId, ) -> QueryResult where @@ -1223,9 +1254,9 @@ where |_, _, _, continuation_type| Ok(continuation_type), )?; - let _ = unification::subtype(state, context, accumulated_type, result_type)?; + let _ = unification::subtype(state, context, continuation_type, result_type)?; - Ok(accumulated_type) + Ok(continuation_type) } /// Looks up the type of a term. diff --git a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap index 2d5f239b..919bd3de 100644 --- a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -16,7 +17,7 @@ discard :: (m :: Type -> Type) (b :: Type) pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) test :: forall (m :: Type -> Type). (m :: Type -> Type) (Tuple Int String) -test' :: forall (t58 :: Type -> Type). (t58 :: Type -> Type) (Tuple Int String) +test' :: forall (t56 :: Type -> Type). (t56 :: Type -> Type) (Tuple Int String) Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index ccf632a1..4d924c0b 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -34,17 +35,17 @@ bind :: testDo :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) (Tuple Int String) testDo' :: - forall (t55 :: Type -> Type). - Bind (t55 :: Type -> Type) => (t55 :: Type -> Type) (Tuple Int String) + forall (t53 :: Type -> Type). + Bind (t53 :: Type -> Type) => (t53 :: Type -> Type) (Tuple Int String) testAdo :: forall (f :: Type -> Type). Applicative (f :: Type -> Type) => (f :: Type -> Type) (Tuple Int String) testAdo' :: - forall (t85 :: Type -> Type). - Applicative (t85 :: Type -> Type) => (t85 :: Type -> Type) (Tuple Int String) + forall (t87 :: Type -> Type). + Applicative (t87 :: Type -> Type) => (t87 :: Type -> Type) (Tuple Int String) testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int testDoDiscard' :: - forall (t101 :: Type -> Type). Discard (t101 :: Type -> Type) => (t101 :: Type -> Type) Int + forall (t103 :: Type -> Type). Discard (t103 :: Type -> Type) => (t103 :: Type -> Type) Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/215_do_bind_error/Main.snap b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap index 7dfe42e7..279af6da 100644 --- a/tests-integration/fixtures/checking/215_do_bind_error/Main.snap +++ b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap @@ -21,4 +21,4 @@ Roles Effect = [Nominal] Diagnostics -error[CannotUnify] at 359..370: Cannot unify 'String' with 'Int' +error[CannotUnify] at 385..386: Cannot unify 'String' with 'Int' From adab506cc77bc1d414b32d0f968b612d427eef98 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 22 Jan 2026 23:33:49 +0800 Subject: [PATCH 018/386] Improve inference for ado statements --- compiler-core/checking/src/algorithm/term.rs | 156 ++++++++++++------ compiler-core/checking/src/error.rs | 3 + compiler-core/checking/src/trace.rs | 3 + compiler-core/diagnostics/src/context.rs | 3 + .../checking/066_ado_collector/Main.snap | 11 +- .../checking/117_do_ado_constrained/Main.snap | 2 +- .../checking/216_ado_bind_error/Main.purs | 13 ++ .../checking/216_ado_bind_error/Main.snap | 24 +++ tests-integration/tests/checking/generated.rs | 2 + 9 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 tests-integration/fixtures/checking/216_ado_bind_error/Main.purs create mode 100644 tests-integration/fixtures/checking/216_ado_bind_error/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 03e66c15..04d26e3b 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -659,9 +659,9 @@ where // do statements rather than the pure_expression itself. // // main = do - // pure 42 - // y <- pure "Hello!" - // pure $ Message y + // pure 42 + // y <- pure "Hello!" + // pure $ Message y // // pure_expression :: Effect Message // pure_expression_type := ?pure_expression @@ -680,8 +680,6 @@ where // inferred expression type and a function type synthesised using // the unification variables we created previously. // - // First, the bind statement: - // // expression_type := Effect String // // lambda_type := ?y -> continuation_type @@ -696,7 +694,8 @@ where // // continuation_type := Effect ?b // - // Then, the discard statement: + // Then, the infer_do_discard rule applies `discard` to the + // inferred expression type and extracts the result type. // // expression_type := Effect Int // @@ -729,6 +728,7 @@ where Ok(continuation_type) } +#[tracing::instrument(skip_all, name = "infer_ado")] fn infer_ado( state: &mut CheckState, context: &CheckContext, @@ -774,6 +774,10 @@ where assert_eq!(binder_types.len(), ado_statements.len()); + // For ado blocks with no bindings, we apply pure to the expression. + // + // pure_type := a -> f a + // expression := t let [head_statement, tail_statements @ ..] = &ado_statements[..] else { return if let Some(expression) = expression { check_function_term_application(state, context, pure_type, expression) @@ -783,57 +787,77 @@ where }; }; - // With the binders and let-bound names in scope, infer - // the type of the final expression as our starting point. // - // main = ado - // pure 1 - // y <- pure "Hello!" - // in Message y + // Create a fresh unification variable for the in_expression. + // Inferring expression directly may solve the unification variables + // introduced in the first pass. This is undesirable, because the + // errors would be attributed incorrectly to the ado statements + // rather than the in-expression itself. // - // expression_type := Message - let expression_type = if let Some(expression) = expression { - infer_expression(state, context, expression)? - } else { - state.fresh_unification_type(context) - }; - - // Create a function type using the binder types collected - // from the forward pass. We made sure to allocate unification - // variables for the discard statements too. + // ado + // a <- pure "Hello!" + // _ <- pure 42 + // in Message a // - // lambda_type := ?discard -> ?y -> Message - let lambda_type = state.make_function(&binder_types, expression_type); - - // This applies map_type to the lambda_type that we just built - // and then to the inferred type of the first expression. + // in_expression :: Effect Message + // in_expression_type := ?in_expression + // lambda_type := ?a -> ?b -> ?in_expression + let in_expression_type = state.fresh_unification_type(context); + let lambda_type = state.make_function(&binder_types, in_expression_type); + + // The desugared form of an ado-expression is a forward applicative + // pipeline, unlike do-notation which works inside-out. The example + // above desugars to the following expression: + // + // (\a _ -> Message a) <$> (pure "Hello!") <*> (pure 42) // - // map_type := (a -> b) -> f a -> f b - // lambda_type := ?discard -> ?y -> Message + // To emulate this, the infer_ado_map rule applies `map` to the + // inferred expression type and the lambda type we synthesised + // using the unification variables we created previously. // - // map_applied := f ?discard -> f (?y -> Message) - // expression_type := f Int + // map_type :: (a -> b) -> f a -> f b + // lambda_type := ?a -> ?b -> ?in_expression // - // accumulated_type := f (?y -> Message) - let mut accumulated_type = + // expression_type := Effect String + // map(lambda, expression) := Effect (?b -> ?in_expression) + // >> + // >> ?a := String + // + // continuation_type := Effect (?b -> ?in_expression) + let mut continuation_type = infer_ado_map(state, context, map_type, lambda_type, *head_statement)?; - // This applies apply_type to the accumulated_type, and then to the - // inferred type of the expression to update the accumulated_type. + // Then, the infer_ado_apply rule applies `apply` to the inferred + // expression type and the continuation type that is a function + // contained within some container, like Effect. // - // apply_type := f (a -> b) -> f a -> f b - // accumulated_type := f (?y -> Message) + // apply_type := f (x -> y) -> f x -> f y + // continuation_type := Effect (?b -> ?in_expression) // - // accumulated_type := f ?y -> f Message - // expression_type := f String + // expression_type := Effect Int + // apply(continuation, expression) := Effect ?in_expression + // >> + // >> ?b := Int // - // accumulated_type := f Message + // continuation_type := Effect ?in_expression for expression in tail_statements { - accumulated_type = - infer_ado_apply(state, context, apply_type, accumulated_type, *expression)?; + continuation_type = + infer_ado_apply(state, context, apply_type, continuation_type, *expression)?; } - Ok(accumulated_type) + // Finally, check the in-expression against in_expression. + // At this point the binder unification variables have been solved + // to concrete types, so errors are attributed to the in-expression. + // + // in_expression :: Effect Message + // in_expression_type := Effect ?in_expression + // >> + // >> ?in_expression := Message + if let Some(expression) = expression { + check_expression(state, context, expression, in_expression_type)?; + } + + Ok(continuation_type) } fn infer_array( @@ -1040,11 +1064,12 @@ where } } +#[tracing::instrument(skip_all, name = "infer_ado_map")] fn infer_ado_map( state: &mut CheckState, context: &CheckContext, map_type: TypeId, - lambda_type: TypeId, + continuation_type: TypeId, expression: Option, ) -> QueryResult where @@ -1053,7 +1078,21 @@ where let Some(expression) = expression else { return Ok(context.prim.unknown); }; + state.with_error_step(ErrorStep::InferringAdoMap(expression), |state| { + infer_ado_map_core(state, context, map_type, continuation_type, expression) + }) +} +fn infer_ado_map_core( + state: &mut CheckState, + context: &CheckContext, + map_type: TypeId, + lambda_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ // expression_type := f a let expression_type = infer_expression(state, context, expression)?; @@ -1085,11 +1124,12 @@ where ) } +#[tracing::instrument(skip_all, name = "infer_ado_apply")] fn infer_ado_apply( state: &mut CheckState, context: &CheckContext, apply_type: TypeId, - accumulated_type: TypeId, + continuation_type: TypeId, expression: Option, ) -> QueryResult where @@ -1098,20 +1138,34 @@ where let Some(expression) = expression else { return Ok(context.prim.unknown); }; + state.with_error_step(ErrorStep::InferringAdoApply(expression), |state| { + infer_ado_apply_core(state, context, apply_type, continuation_type, expression) + }) +} +fn infer_ado_apply_core( + state: &mut CheckState, + context: &CheckContext, + apply_type: TypeId, + continuation_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ // expression_type := ?f ?a let expression_type = infer_expression(state, context, expression)?; - // apply_type := f (a -> b) -> f a -> f b - // accumulated_type := f (a -> b) + // apply_type := f (a -> b) -> f a -> f b + // continuation_type := f (a -> b) let apply_applied = check_function_application_core( state, context, apply_type, - accumulated_type, - |state, context, accumulated_type, expected_type| { - let _ = unification::subtype(state, context, accumulated_type, expected_type)?; - Ok(accumulated_type) + continuation_type, + |state, context, continuation_type, expected_type| { + let _ = unification::subtype(state, context, continuation_type, expected_type)?; + Ok(continuation_type) }, )?; diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 7525a1d8..4f933bed 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -25,6 +25,9 @@ pub enum ErrorStep { InferringDoBind(lowering::ExpressionId), InferringDoDiscard(lowering::ExpressionId), + + InferringAdoMap(lowering::ExpressionId), + InferringAdoApply(lowering::ExpressionId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/compiler-core/checking/src/trace.rs b/compiler-core/checking/src/trace.rs index 44247a3c..19ddf1b9 100644 --- a/compiler-core/checking/src/trace.rs +++ b/compiler-core/checking/src/trace.rs @@ -35,6 +35,9 @@ where ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { context.stabilized.syntax_ptr(*id)? } + ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { + context.stabilized.syntax_ptr(*id)? + } }; let range = pointer.text_range(); diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index 4d028a78..c47a5fd1 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -121,6 +121,9 @@ impl<'a> DiagnosticsContext<'a> { ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { self.stabilized.syntax_ptr(*id)? } + ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { + self.stabilized.syntax_ptr(*id)? + } }; self.span_from_syntax_ptr(&ptr) } diff --git a/tests-integration/fixtures/checking/066_ado_collector/Main.snap b/tests-integration/fixtures/checking/066_ado_collector/Main.snap index 15125111..35600fc8 100644 --- a/tests-integration/fixtures/checking/066_ado_collector/Main.snap +++ b/tests-integration/fixtures/checking/066_ado_collector/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -15,15 +16,15 @@ apply :: Collector (Tuple (x :: Type) (y :: Type)) (b :: Type) pure :: forall (a :: Type). (a :: Type) -> Collector (a :: Type) (a :: Type) test1 :: - forall (t18 :: Type). + forall (t19 :: Type). Collector - (Tuple (Tuple (Tuple Int (t18 :: Type)) String) Char) + (Tuple (Tuple (Tuple Int (t19 :: Type)) String) Char) { x :: Int, y :: String, z :: Char } test2 :: - forall (t37 :: Type). - Collector (Tuple (Tuple Char (t37 :: Type)) Boolean) { x :: Char, y :: Boolean } + forall (t39 :: Type). + Collector (Tuple (Tuple Char (t39 :: Type)) Boolean) { x :: Char, y :: Boolean } test3 :: - forall (t51 :: Type). Collector (Tuple (Tuple Int (t51 :: Type)) String) { x :: Int, z :: String } + forall (t54 :: Type). Collector (Tuple (Tuple Int (t54 :: Type)) String) { x :: Int, z :: String } Types Collector :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 4d924c0b..3d683bee 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -45,7 +45,7 @@ testAdo' :: Applicative (t87 :: Type -> Type) => (t87 :: Type -> Type) (Tuple Int String) testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int testDoDiscard' :: - forall (t103 :: Type -> Type). Discard (t103 :: Type -> Type) => (t103 :: Type -> Type) Int + forall (t105 :: Type -> Type). Discard (t105 :: Type -> Type) => (t105 :: Type -> Type) Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/216_ado_bind_error/Main.purs b/tests-integration/fixtures/checking/216_ado_bind_error/Main.purs new file mode 100644 index 00000000..ec5f32eb --- /dev/null +++ b/tests-integration/fixtures/checking/216_ado_bind_error/Main.purs @@ -0,0 +1,13 @@ +module Main where + +foreign import data Effect :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b +foreign import add :: Int -> Int -> Int + +test = ado + a <- pure 123456 + b <- pure "life" + in add a b diff --git a/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap b/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap new file mode 100644 index 00000000..bb13f0e2 --- /dev/null +++ b/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +add :: Int -> Int -> Int +test :: Effect Int + +Types +Effect :: Type -> Type + +Roles +Effect = [Nominal] + +Diagnostics +error[CannotUnify] at 354..355: Cannot unify 'String' with 'Int' diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a89bcdb7..21efa0ca 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -457,3 +457,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_214_row_nub_left_bias_main() { run_test("214_row_nub_left_bias", "Main"); } #[rustfmt::skip] #[test] fn test_215_do_bind_error_main() { run_test("215_do_bind_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_216_ado_bind_error_main() { run_test("216_ado_bind_error", "Main"); } From 6ce85b2bb76368235706f9de0dcd096bb75d574e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 03:05:44 +0800 Subject: [PATCH 019/386] Normalise files when loading --- tests-integration/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests-integration/src/lib.rs b/tests-integration/src/lib.rs index 0cb79b63..795c2631 100644 --- a/tests-integration/src/lib.rs +++ b/tests-integration/src/lib.rs @@ -12,6 +12,7 @@ use url::Url; fn load_file(engine: &mut QueryEngine, files: &mut Files, path: &Path) { let url = Url::from_file_path(path).unwrap(); let file = fs::read_to_string(path).unwrap(); + let file = file.replace("\r\n", "\n"); let uri = url.to_string(); let id = files.insert(uri, file); From 2fbe7740233a19531aa2754063a217e2c89703b7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 03:17:11 +0800 Subject: [PATCH 020/386] Add basic tests for monad mismatch --- .../checking/217_do_monad_error/Main.purs | 18 +++++++++++++ .../checking/217_do_monad_error/Main.snap | 27 +++++++++++++++++++ .../checking/218_ado_monad_error/Main.purs | 17 ++++++++++++ .../checking/218_ado_monad_error/Main.snap | 27 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 4 +++ 5 files changed, 93 insertions(+) create mode 100644 tests-integration/fixtures/checking/217_do_monad_error/Main.purs create mode 100644 tests-integration/fixtures/checking/217_do_monad_error/Main.snap create mode 100644 tests-integration/fixtures/checking/218_ado_monad_error/Main.purs create mode 100644 tests-integration/fixtures/checking/218_ado_monad_error/Main.snap diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.purs b/tests-integration/fixtures/checking/217_do_monad_error/Main.purs new file mode 100644 index 00000000..84093e47 --- /dev/null +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.purs @@ -0,0 +1,18 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Aff :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import affPure :: forall a. a -> Aff a + +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +test :: Effect { a :: Int, b :: String, c :: Int } +test = do + a <- pure 123456 + b <- affPure "life" + c <- pure 123456 + pure { a, b, c } + diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap new file mode 100644 index 00000000..2c02a32c --- /dev/null +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +affPure :: forall (a :: Type). (a :: Type) -> Aff (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +test :: Effect { a :: Int, b :: String, c :: Int } + +Types +Effect :: Type -> Type +Aff :: Type -> Type + +Roles +Effect = [Nominal] +Aff = [Nominal] + +Diagnostics +error[CannotUnify] at 439..453: Cannot unify 'Aff' with 'Effect' +error[CannotUnify] at 439..453: Cannot unify 'Aff String' with 'Effect ?15[:0]' diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs b/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs new file mode 100644 index 00000000..7e293ab9 --- /dev/null +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs @@ -0,0 +1,17 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Aff :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import affPure :: forall a. a -> Aff a + +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +test :: Effect { a :: Int, b :: String, c :: Int } +test = ado + a <- pure 123456 + b <- affPure "life" + c <- pure 123456 + in { a, b, c } diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap new file mode 100644 index 00000000..c9b78f7c --- /dev/null +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +affPure :: forall (a :: Type). (a :: Type) -> Aff (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +test :: Effect { a :: Int, b :: String, c :: Int } + +Types +Effect :: Type -> Type +Aff :: Type -> Type + +Roles +Effect = [Nominal] +Aff = [Nominal] + +Diagnostics +error[CannotUnify] at 430..444: Cannot unify 'Aff' with 'Effect' +error[CannotUnify] at 430..444: Cannot unify 'Aff String' with 'Effect ?15[:0]' diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 21efa0ca..b4633fbd 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -459,3 +459,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_215_do_bind_error_main() { run_test("215_do_bind_error", "Main"); } #[rustfmt::skip] #[test] fn test_216_ado_bind_error_main() { run_test("216_ado_bind_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_217_do_monad_error_main() { run_test("217_do_monad_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_218_ado_monad_error_main() { run_test("218_ado_monad_error", "Main"); } From 3c05bb6e5859b8538e3551745874a9c0df5c1247 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 03:31:50 +0800 Subject: [PATCH 021/386] Better reporting for errors in failure tests --- compiler-core/diagnostics/src/lib.rs | 2 +- compiler-core/diagnostics/src/render.rs | 104 +++++++++++++++++- .../032_recursive_synonym_expansion/Main.snap | 55 +++++++-- .../078_inspect_arity_invalid/Main.snap | 19 +++- .../080_let_recursive_errors/Main.snap | 31 +++++- .../checking/084_instance_eq/Main.snap | 7 +- .../checking/089_no_instance_found/Main.snap | 7 +- .../092_ambiguous_constraint/Main.snap | 13 ++- .../Main.snap | 7 +- .../Main.snap | 7 +- .../111_int_add_invalid_no_instance/Main.snap | 7 +- .../112_int_mul_invalid_no_instance/Main.snap | 7 +- .../Main.snap | 13 ++- .../Main.snap | 7 +- .../checking/115_empty_do_block/Main.snap | 13 ++- .../checking/116_empty_ado_block/Main.snap | 13 ++- .../checking/117_do_ado_constrained/Main.snap | 6 +- .../Main.snap | 18 ++- .../123_incomplete_instance_head/Main.snap | 6 +- .../Main.snap | 6 +- .../Main.snap | 12 +- .../checking/127_derive_eq_simple/Main.snap | 7 +- .../131_derive_eq_missing_instance/Main.snap | 7 +- .../132_derive_eq_1_higher_kinded/Main.snap | 7 +- .../checking/133_derive_eq_partial/Main.snap | 7 +- .../checking/134_derive_ord_simple/Main.snap | 7 +- .../135_derive_ord_1_higher_kinded/Main.snap | 13 ++- .../136_derive_nested_higher_kinded/Main.snap | 25 ++++- .../142_derive_newtype_not_newtype/Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 13 ++- .../Main.snap | 13 ++- .../153_derive_contravariant_error/Main.snap | 13 ++- .../155_derive_profunctor_error/Main.snap | 19 +++- .../Main.snap | 13 ++- .../Main.snap | 25 ++++- .../Main.snap | 7 +- .../Main.snap | 13 ++- .../Main.snap | 13 ++- .../checking/167_derive_eq_1/Main.snap | 13 ++- .../checking/168_derive_ord_1/Main.snap | 19 +++- .../Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 7 +- .../checking/190_coercible_nominal/Main.snap | 7 +- .../191_coercible_newtype_hidden/Main.snap | 25 ++++- .../Main.snap | 13 ++- .../Main.snap | 7 +- .../202_int_compare_invalid/Main.snap | 43 ++++++-- .../checking/205_builtin_warn/Main.snap | 49 +++++++-- .../checking/206_builtin_fail/Main.snap | 13 ++- .../checking/215_do_bind_error/Main.snap | 6 +- .../checking/216_ado_bind_error/Main.snap | 6 +- .../checking/217_do_monad_error/Main.snap | 12 +- .../checking/218_ado_monad_error/Main.snap | 12 +- tests-integration/src/generated/basic.rs | 4 +- 59 files changed, 728 insertions(+), 119 deletions(-) diff --git a/compiler-core/diagnostics/src/lib.rs b/compiler-core/diagnostics/src/lib.rs index 96beec57..60c6470c 100644 --- a/compiler-core/diagnostics/src/lib.rs +++ b/compiler-core/diagnostics/src/lib.rs @@ -6,4 +6,4 @@ mod render; pub use context::DiagnosticsContext; pub use convert::ToDiagnostics; pub use model::{Diagnostic, DiagnosticCode, RelatedSpan, Severity, Span}; -pub use render::{format_text, to_lsp_diagnostic}; +pub use render::{format_rustc, format_text, to_lsp_diagnostic}; diff --git a/compiler-core/diagnostics/src/render.rs b/compiler-core/diagnostics/src/render.rs index 5ebbde29..480b8cd0 100644 --- a/compiler-core/diagnostics/src/render.rs +++ b/compiler-core/diagnostics/src/render.rs @@ -5,7 +5,7 @@ use lsp_types::{ }; use rowan::TextSize; -use crate::{Diagnostic, Severity}; +use crate::{Diagnostic, Severity, Span}; pub fn format_text(diagnostics: &[Diagnostic]) -> String { let mut output = String::new(); @@ -32,6 +32,108 @@ pub fn format_text(diagnostics: &[Diagnostic]) -> String { output } +fn line_text<'a>(line_index: &LineIndex, content: &'a str, line: u32) -> Option<&'a str> { + let range = line_index.line(line)?; + let text = &content[range]; + Some(text.trim_end_matches(['\n', '\r'])) +} + +fn caret_marker(line: &str, start_col: u32, end_col: Option) -> String { + let line_len = line.chars().count() as u32; + let start = start_col.min(line_len); + let end = end_col.unwrap_or(line_len).min(line_len); + + if end <= start { + format!("{}^", " ".repeat(start as usize)) + } else { + let tilde_count = (end - start).saturating_sub(1) as usize; + format!("{}^{}", " ".repeat(start as usize), "~".repeat(tilde_count)) + } +} + +fn span_location( + line_index: &LineIndex, + content: &str, + span: Span, +) -> Option<((u32, u32), (u32, u32))> { + let start = offset_to_position(line_index, content, TextSize::from(span.start))?; + let end = offset_to_position(line_index, content, TextSize::from(span.end))?; + Some(((start.line, start.character), (end.line, end.character))) +} + +pub fn format_rustc(diagnostics: &[Diagnostic], content: &str) -> String { + let line_index = LineIndex::new(content); + let mut output = String::new(); + + for diagnostic in diagnostics { + let severity = match diagnostic.severity { + Severity::Error => "error", + Severity::Warning => "warning", + }; + + output.push_str(&format!("{severity}[{}]: {}\n", diagnostic.code, diagnostic.message)); + + if let Some(((start_line, start_col), (end_line, end_col))) = + span_location(&line_index, content, diagnostic.primary) + { + let display_start_line = start_line + 1; + let display_start_col = start_col + 1; + let display_end_line = end_line + 1; + let display_end_col = end_col + 1; + + output.push_str(&format!( + " --> {}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + + if let Some(line) = line_text(&line_index, content, start_line) { + let line_num_width = display_start_line.to_string().len(); + output.push_str(&format!("{:>width$} |\n", "", width = line_num_width)); + output.push_str(&format!("{} | {}\n", display_start_line, line)); + + let marker_end_col = if start_line == end_line { Some(end_col) } else { None }; + let marker = caret_marker(line, start_col, marker_end_col); + output.push_str(&format!("{:>width$} | {}\n", "", marker, width = line_num_width)); + } + } + + for related in &diagnostic.related { + output.push_str(&format!(" note: {}\n", related.message)); + + if let Some(((start_line, start_col), (end_line, end_col))) = + span_location(&line_index, content, related.span) + { + let display_start_line = start_line + 1; + let display_start_col = start_col + 1; + let display_end_line = end_line + 1; + let display_end_col = end_col + 1; + + output.push_str(&format!( + " --> {}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + + if let Some(line) = line_text(&line_index, content, start_line) { + let line_num_width = display_start_line.to_string().len(); + output.push_str(&format!(" {:>width$} |\n", "", width = line_num_width)); + output.push_str(&format!(" {} | {}\n", display_start_line, line)); + + let marker_end_col = if start_line == end_line { Some(end_col) } else { None }; + let marker = caret_marker(line, start_col, marker_end_col); + output.push_str(&format!( + " {:>width$} | {}\n", + "", + marker, + width = line_num_width + )); + } + } + } + } + + output +} + fn offset_to_position(line_index: &LineIndex, content: &str, offset: TextSize) -> Option { let LineCol { line, col } = line_index.line_col(offset); diff --git a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap index 752befd7..1e5cca2f 100644 --- a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -37,12 +38,48 @@ Valid = Int Diagnostics -error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 54..69: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 83..98: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion -error[RecursiveSynonymExpansion] at 112..127: Recursive type synonym expansion +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 8:1..8:16 + | +8 | testF :: F -> F + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 8:1..8:16 + | +8 | testF :: F -> F + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 8:1..8:16 + | +8 | testF :: F -> F + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 11:1..11:16 + | +11 | testG :: G -> G + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 11:1..11:16 + | +11 | testG :: G -> G + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 11:1..11:16 + | +11 | testG :: G -> G + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 14:1..14:16 + | +14 | testH :: H -> H + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 14:1..14:16 + | +14 | testH :: H -> H + | ^~~~~~~~~~~~~~~ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 14:1..14:16 + | +14 | testH :: H -> H + | ^~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap index 3d2115ae..2f40d2ee 100644 --- a/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap +++ b/tests-integration/fixtures/checking/078_inspect_arity_invalid/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -36,6 +37,18 @@ Const = forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type Diagnostics -error[TooManyBinders] at 94..126: Too many binders: expected 2, got 3 -error[TooManyBinders] at 208..247: Too many binders: expected 2, got 3 -error[TooManyBinders] at 310..329: Too many binders: expected 2, got 3 +error[TooManyBinders]: Too many binders: expected 2, got 3 + --> 7:1..7:33 + | +7 | addWrong :: forall a. BinaryOp a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[TooManyBinders]: Too many binders: expected 2, got 3 + --> 13:1..13:40 + | +13 | composeWrong :: forall a. ReturnUnary a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[TooManyBinders]: Too many binders: expected 2, got 3 + --> 18:1..18:20 + | +18 | constWrong :: Const + | ^~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index 80e041ad..f6ecb1ec 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -12,8 +13,28 @@ threeWayConflict :: Int -> Int Types Diagnostics -error[CannotUnify] at 163..248: Cannot unify 'Int' with 'String' -error[CannotUnify] at 163..248: Cannot unify 'String' with 'Int' -error[CannotUnify] at 356..376: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' -error[CannotUnify] at 521..522: Cannot unify 'Int' with 'String' -error[CannotUnify] at 660..666: Cannot unify 'String' with 'Int' +error[CannotUnify]: Cannot unify 'Int' with 'String' + --> 8:3..12:9 + | +8 | let f :: Int -> Int + | ^~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 8:3..12:9 + | +8 | let f :: Int -> Int + | ^~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' + --> 17:3..18:9 + | +17 | let f x = f + | ^~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Int' with 'String' + --> 25:8..25:9 + | +25 | in f n + | ^ +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 32:15..32:21 + | +32 | b x = c "oops" + | ^~~~~~ diff --git a/tests-integration/fixtures/checking/084_instance_eq/Main.snap b/tests-integration/fixtures/checking/084_instance_eq/Main.snap index 995de79f..8da71424 100644 --- a/tests-integration/fixtures/checking/084_instance_eq/Main.snap +++ b/tests-integration/fixtures/checking/084_instance_eq/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -17,4 +18,8 @@ instance Eq (Int :: Type) chain: 0 Diagnostics -error[NoInstanceFound] at 103..138: No instance found for: Eq String +error[NoInstanceFound]: No instance found for: Eq String + --> 9:1..9:36 + | +9 | test = [eq 123 456, eq "123" "456"] + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap index 55ed18f4..c7b8f65a 100644 --- a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap +++ b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -24,4 +25,8 @@ Classes class Eq (&0 :: Type) Diagnostics -error[NoInstanceFound] at 127..149: No instance found for: Eq Foo +error[NoInstanceFound]: No instance found for: Eq Foo + --> 9:1..9:23 + | +9 | test :: Foo -> Boolean + | ^~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap index 43cd583e..bfe8a594 100644 --- a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap +++ b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -16,5 +17,13 @@ class Read (&0 :: Type) class Show (&0 :: Type) Diagnostics -error[AmbiguousConstraint] at 235..257: Ambiguous constraint: Show ?5[:0] -error[AmbiguousConstraint] at 235..257: Ambiguous constraint: Read ?5[:0] +error[AmbiguousConstraint]: Ambiguous constraint: Show ?5[:0] + --> 11:1..11:23 + | +11 | test s = show (read s) + | ^~~~~~~~~~~~~~~~~~~~~~ +error[AmbiguousConstraint]: Ambiguous constraint: Read ?5[:0] + --> 11:1..11:23 + | +11 | test s = show (read s) + | ^~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap index 2f20d1fa..120a0d4a 100644 --- a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap +++ b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -24,4 +25,8 @@ Classes class Eq (&0 :: Type) Diagnostics -error[NoInstanceFound] at 191..227: No instance found for: Eq Foo +error[NoInstanceFound]: No instance found for: Eq Foo + --> 10:1..12:11 + | +10 | test = + | ^~~~~~ diff --git a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap index 8cdb2334..7da6b0bd 100644 --- a/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/110_row_lacks_invalid_no_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,4 +21,8 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 198..222: No instance found for: Lacks @Type "b" ( a :: Int, b :: String ) +error[NoInstanceFound]: No instance found for: Lacks @Type "b" ( a :: Int, b :: String ) + --> 11:1..11:25 + | +11 | forceSolve = { invalid } + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap index df98ccc9..8e8d9360 100644 --- a/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/111_int_add_invalid_no_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,4 +21,8 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 154..178: No instance found for: Add 2 3 10 +error[NoInstanceFound]: No instance found for: Add 2 3 10 + --> 11:1..11:25 + | +11 | forceSolve = { invalid } + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap index 57fc47d3..193d796b 100644 --- a/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/112_int_mul_invalid_no_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,4 +21,8 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 154..178: No instance found for: Mul 2 3 10 +error[NoInstanceFound]: No instance found for: Mul 2 3 10 + --> 11:1..11:25 + | +11 | forceSolve = { invalid } + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap index fdc222ff..dcd76544 100644 --- a/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/113_int_compare_invalid_no_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,5 +21,13 @@ Roles Proxy = [Phantom] Diagnostics -error[InvalidImportItem] at 64..68: Cannot import item 'kind' -error[NoInstanceFound] at 199..223: No instance found for: Compare 5 1 LT +error[InvalidImportItem]: Cannot import item 'kind' + --> 4:23..4:27 + | +4 | import Prim.Ordering (kind Ordering, LT) + | ^~~~ +error[NoInstanceFound]: No instance found for: Compare 5 1 LT + --> 12:1..12:25 + | +12 | forceSolve = { invalid } + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap index d3ebec02..2d9ef469 100644 --- a/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap +++ b/tests-integration/fixtures/checking/114_int_tostring_invalid_no_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,4 +21,8 @@ Roles Proxy = [Phantom] Diagnostics -error[NoInstanceFound] at 164..188: No instance found for: ToString 42 "999" +error[NoInstanceFound]: No instance found for: ToString 42 "999" + --> 11:1..11:25 + | +11 | forceSolve = { invalid } + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap index 062a534e..9ca9b172 100644 --- a/tests-integration/fixtures/checking/115_empty_do_block/Main.snap +++ b/tests-integration/fixtures/checking/115_empty_do_block/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -19,5 +20,13 @@ Roles Effect = [Nominal] Diagnostics -error[EmptyDoBlock] at 271..273: Empty do block -error[CannotUnify] at 264..273: Cannot unify 'Type' with '???' +error[EmptyDoBlock]: Empty do block + --> 9:8..9:10 + | +9 | test = do + | ^~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 9:1..9:10 + | +9 | test = do + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap index c6a4737d..7262504d 100644 --- a/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap +++ b/tests-integration/fixtures/checking/116_empty_ado_block/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -20,5 +21,13 @@ Roles Effect = [Nominal] Diagnostics -error[EmptyAdoBlock] at 262..265: Empty ado block -error[CannotUnify] at 254..265: Cannot unify 'Type' with '???' +error[EmptyAdoBlock]: Empty ado block + --> 9:9..9:12 + | +9 | test1 = ado + | ^~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 9:1..9:12 + | +9 | test1 = ado + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 3d683bee..f4dd61f9 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -74,4 +74,8 @@ class Applicative (&0 :: Type -> Type) <= Bind (&0 :: Type -> Type) class Bind (&0 :: Type -> Type) <= Monad (&0 :: Type -> Type) Diagnostics -error[NoInstanceFound] at 835..878: No instance found for: Discard (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Discard (&0 :: Type -> Type) + --> 44:1..44:44 + | +44 | testDoDiscard :: forall m. Monad m => m Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap index 704f693c..b52dd777 100644 --- a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap +++ b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap @@ -17,6 +17,18 @@ instance Show (Int :: Type) chain: 0 Diagnostics -error[CannotUnify] at 61..118: Cannot unify 'Int' with 'String' -error[CannotUnify] at 61..118: Cannot unify 'Int -> Int' with 'Int -> String' -error[InstanceMemberTypeMismatch] at 61..118: Instance member type mismatch: expected 'Int -> String', got 'Int -> Int' +error[CannotUnify]: Cannot unify 'Int' with 'String' + --> 6:1..8:13 + | +6 | instance Show Int where + | ^~~~~~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Int -> Int' with 'Int -> String' + --> 6:1..8:13 + | +6 | instance Show Int where + | ^~~~~~~~~~~~~~~~~~~~~~~ +error[InstanceMemberTypeMismatch]: Instance member type mismatch: expected 'Int -> String', got 'Int -> Int' + --> 6:1..8:13 + | +6 | instance Show Int where + | ^~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap index 72bdf9b8..4428d144 100644 --- a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap +++ b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap @@ -22,4 +22,8 @@ instance Pair (Int :: Type) chain: 0 Diagnostics -error[InstanceHeadMismatch] at 140..191: Instance head mismatch: expected 2 arguments, got 1 +error[InstanceHeadMismatch]: Instance head mismatch: expected 2 arguments, got 1 + --> 9:1..10:28 + | +9 | instance Pair Int where + | ^~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap index e3e88525..400fb72d 100644 --- a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap +++ b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap @@ -36,4 +36,8 @@ instance Functor (Box :: Type -> Type) chain: 0 Diagnostics -error[NoInstanceFound] at 148..240: No instance found for: Show (~&1 :: Type) +error[NoInstanceFound]: No instance found for: Show (~&1 :: Type) + --> 11:1..16:16 + | +11 | instance Functor Box where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap index ba893cfe..467ed8e3 100644 --- a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap +++ b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap @@ -17,5 +17,13 @@ instance Show (Boolean :: Type) chain: 0 Diagnostics -error[CannotUnify] at 61..138: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' -error[InstanceMemberTypeMismatch] at 61..138: Instance member type mismatch: expected 'Boolean -> String', got 'forall (a :: Type). (a :: Type) -> String' +error[CannotUnify]: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' + --> 6:1..8:18 + | +6 | instance Show Boolean where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[InstanceMemberTypeMismatch]: Instance member type mismatch: expected 'Boolean -> String', got 'forall (a :: Type). (a :: Type) -> String' + --> 6:1..8:18 + | +6 | instance Show Boolean where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap index d683506c..c76f2e62 100644 --- a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap +++ b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -36,4 +37,8 @@ derive forall (&0 :: Type). Eq (Proxy @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type derive Eq (ContainsNoEq :: Type) Diagnostics -error[NoInstanceFound] at 159..190: No instance found for: Eq NoEq +error[NoInstanceFound]: No instance found for: Eq NoEq + --> 13:1..13:32 + | +13 | derive instance Eq ContainsNoEq + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap index 961735a6..99f1b9c8 100644 --- a/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/131_derive_eq_missing_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -28,4 +29,8 @@ Derived derive Eq (Box :: Type) Diagnostics -error[NoInstanceFound] at 89..111: No instance found for: Eq NoEq +error[NoInstanceFound]: No instance found for: Eq NoEq + --> 9:1..9:23 + | +9 | derive instance Eq Box + | ^~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap index b5766977..a43e9f9e 100644 --- a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -35,4 +36,8 @@ derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type derive Eq (&1 :: Type) => Eq (WrapNoEq1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 216..258: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Eq1 (&0 :: Type -> Type) + --> 12:1..12:43 + | +12 | derive instance Eq a => Eq (WrapNoEq1 f a) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap index a0ceb80b..7152bcf6 100644 --- a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap +++ b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -23,4 +24,8 @@ derive Eq (&0 :: Type) => Eq (Either Int (&0 :: Type) :: Type) derive Eq (Either Int (&0 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 125..158: No instance found for: Eq (&0 :: Type) +error[NoInstanceFound]: No instance found for: Eq (&0 :: Type) + --> 9:1..9:34 + | +9 | derive instance Eq (Either Int b) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap index 809dbb7c..b3a04cd5 100644 --- a/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap +++ b/tests-integration/fixtures/checking/134_derive_ord_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -48,4 +49,8 @@ derive Eq (ContainsNoOrd :: Type) derive Ord (ContainsNoOrd :: Type) Diagnostics -error[NoInstanceFound] at 351..384: No instance found for: Ord NoOrd +error[NoInstanceFound]: No instance found for: Ord NoOrd + --> 23:1..23:34 + | +23 | derive instance Ord ContainsNoOrd + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap index 39e64ab7..3d9665d4 100644 --- a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -37,5 +38,13 @@ derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (WrapNoOrd1 @Type (&0 : derive Ord (&1 :: Type) => Ord (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 320..365: No instance found for: Ord1 (&0 :: Type -> Type) -error[NoInstanceFound] at 320..365: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Ord1 (&0 :: Type -> Type) + --> 14:1..14:46 + | +14 | derive instance Ord a => Ord (WrapNoOrd1 f a) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Eq1 (&0 :: Type -> Type) + --> 14:1..14:46 + | +14 | derive instance Ord a => Ord (WrapNoOrd1 f a) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap index 75494040..9b63086a 100644 --- a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -56,7 +57,23 @@ derive Eq1 (&0 :: Type -> Type) => Eq (U (&0 :: Type -> Type) :: Type) derive Ord1 (&0 :: Type -> Type) => Ord (U (&0 :: Type -> Type) :: Type) Diagnostics -error[NoInstanceFound] at 125..162: No instance found for: Eq ((&1 :: Type -> Type) Int) -error[NoInstanceFound] at 163..202: No instance found for: Ord ((&1 :: Type -> Type) Int) -error[NoInstanceFound] at 446..483: No instance found for: Eq ((&1 :: Int -> Type) 42) -error[NoInstanceFound] at 484..523: No instance found for: Ord ((&1 :: Int -> Type) 42) +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) Int) + --> 8:1..8:38 + | +8 | derive instance (Eq1 f) => Eq (T f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) Int) + --> 9:1..9:40 + | +9 | derive instance (Ord1 f) => Ord (T f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Int -> Type) 42) + --> 23:1..23:38 + | +23 | derive instance (Eq1 f) => Eq (V f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Ord ((&1 :: Int -> Type) 42) + --> 24:1..24:40 + | +24 | derive instance (Ord1 f) => Ord (V f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap index 832a20e9..a46ef31d 100644 --- a/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/142_derive_newtype_not_newtype/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,4 +19,8 @@ Roles Foo = [] Diagnostics -error[ExpectedNewtype] at 70..102: Expected a newtype, got: Foo +error[ExpectedNewtype]: Expected a newtype, got: Foo + --> 7:1..7:33 + | +7 | derive newtype instance Show Foo + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap index 5bfd22fc..9ad82503 100644 --- a/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking/143_derive_newtype_missing_instance/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,4 +22,8 @@ Derived derive Show (Identity String :: Type) Diagnostics -error[NoInstanceFound] at 83..129: No instance found for: Show String +error[NoInstanceFound]: No instance found for: Show String + --> 7:1..7:47 + | +7 | derive newtype instance Show (Identity String) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap index 638aee67..ca2a9ff3 100644 --- a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap +++ b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,4 +22,8 @@ Derived derive Show (Identity (&0 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 83..124: No instance found for: Show (&0 :: Type) +error[NoInstanceFound]: No instance found for: Show (&0 :: Type) + --> 7:1..7:42 + | +7 | derive newtype instance Show (Identity a) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap index 414e82f9..00d74b91 100644 --- a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -35,4 +36,8 @@ derive Functor (&0 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type -> Type) derive Functor (WrapNoFunctor @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 175..216: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Functor (&0 :: Type -> Type) + --> 9:1..9:42 + | +9 | derive instance Functor (WrapNoFunctor f) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap index f832f53e..c4c4bd29 100644 --- a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -40,4 +41,8 @@ derive Functor (Reader (&0 :: Type) :: Type -> Type) derive Functor (Cont (&0 :: Type) :: Type -> Type) Diagnostics -error[ContravariantOccurrence] at 100..133: Type variable occurs in contravariant position: (~&0 :: Type) +error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~&0 :: Type) + --> 6:1..6:34 + | +6 | derive instance Functor Predicate + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap index 16b72bf3..3742d14e 100644 --- a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -51,5 +52,13 @@ derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Bifunctor derive Bifunctor (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound] at 278..330: No instance found for: Functor (&0 :: Type -> Type) -error[NoInstanceFound] at 278..330: No instance found for: Functor (&1 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Functor (&0 :: Type -> Type) + --> 10:1..10:53 + | +10 | derive instance Bifunctor (WrapBothNoConstraint f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Functor (&1 :: Type -> Type) + --> 10:1..10:53 + | +10 | derive instance Bifunctor (WrapBothNoConstraint f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap index aa3d550d..afff3e01 100644 --- a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap +++ b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -32,5 +33,13 @@ Derived derive Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[DeriveMissingFunctor] at 105..145: Deriving Functor requires Data.Functor to be in scope -error[DeriveMissingFunctor] at 105..145: Deriving Functor requires Data.Functor to be in scope +error[DeriveMissingFunctor]: Deriving Functor requires Data.Functor to be in scope + --> 6:1..6:41 + | +6 | derive instance Bifunctor (WrapBoth f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[DeriveMissingFunctor]: Deriving Functor requires Data.Functor to be in scope + --> 6:1..6:41 + | +6 | derive instance Bifunctor (WrapBoth f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap index 90e14254..65de6a52 100644 --- a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -29,5 +30,13 @@ derive Contravariant (Identity :: Type -> Type) derive Contravariant (Producer :: Type -> Type) Diagnostics -error[CovariantOccurrence] at 169..207: Type variable occurs in covariant position: (~&0 :: Type) -error[CovariantOccurrence] at 309..347: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) + --> 7:1..7:39 + | +7 | derive instance Contravariant Identity + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) + --> 11:1..11:39 + | +11 | derive instance Contravariant Producer + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap index c1c34a0b..4edf070e 100644 --- a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -32,6 +33,18 @@ derive Profunctor (WrongFirst :: Type -> Type -> Type) derive Profunctor (WrongSecond :: Type -> Type -> Type) Diagnostics -error[CovariantOccurrence] at 151..188: Type variable occurs in covariant position: (~&0 :: Type) -error[ContravariantOccurrence] at 291..329: Type variable occurs in contravariant position: (~&1 :: Type) -error[CovariantOccurrence] at 291..329: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) + --> 7:1..7:38 + | +7 | derive instance Profunctor WrongFirst + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~&1 :: Type) + --> 11:1..11:39 + | +11 | derive instance Profunctor WrongSecond + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) + --> 11:1..11:39 + | +11 | derive instance Profunctor WrongSecond + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap index d4d07e0a..567f49ea 100644 --- a/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/156_derive_bifunctor_insufficient_params/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -23,5 +24,13 @@ Derived derive Bifunctor (Triple Int String :: Type -> Type) Diagnostics -error[CannotUnify] at 119..138: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 93..138: Cannot derive for type: Triple Int String +error[CannotUnify]: Cannot unify 'Type' with 'Type -> Type' + --> 6:27..6:46 + | +6 | derive instance Bifunctor (Triple Int String) + | ^~~~~~~~~~~~~~~~~~~ +error[CannotDeriveForType]: Cannot derive for type: Triple Int String + --> 6:1..6:46 + | +6 | derive instance Bifunctor (Triple Int String) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap index 468fc94b..2718b5e0 100644 --- a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -29,7 +30,23 @@ derive Functor (Pair Int String :: Type) derive Functor (Unit :: Type) Diagnostics -error[CannotUnify] at 105..122: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 81..122: Cannot derive for type: Pair Int String -error[CannotUnify] at 165..169: Cannot unify 'Type' with 'Type -> Type' -error[CannotDeriveForType] at 141..169: Cannot derive for type: Unit +error[CannotUnify]: Cannot unify 'Type' with 'Type -> Type' + --> 6:25..6:42 + | +6 | derive instance Functor (Pair Int String) + | ^~~~~~~~~~~~~~~~~ +error[CannotDeriveForType]: Cannot derive for type: Pair Int String + --> 6:1..6:42 + | +6 | derive instance Functor (Pair Int String) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with 'Type -> Type' + --> 9:25..9:29 + | +9 | derive instance Functor Unit + | ^~~~ +error[CannotDeriveForType]: Cannot derive for type: Unit + --> 9:1..9:29 + | +9 | derive instance Functor Unit + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap index 7f4852c0..41195b64 100644 --- a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -35,4 +36,8 @@ derive Foldable (&0 :: Type -> Type) => Foldable (Wrap @Type (&0 :: Type -> Type derive Foldable (WrapNoFoldable @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 181..224: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Foldable (&0 :: Type -> Type) + --> 9:1..9:44 + | +9 | derive instance Foldable (WrapNoFoldable f) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap index 13ac9648..f4cc2c0e 100644 --- a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -51,5 +52,13 @@ derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Bifolda derive Bifoldable (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound] at 285..338: No instance found for: Foldable (&0 :: Type -> Type) -error[NoInstanceFound] at 285..338: No instance found for: Foldable (&1 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Foldable (&0 :: Type -> Type) + --> 10:1..10:54 + | +10 | derive instance Bifoldable (WrapBothNoConstraint f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Foldable (&1 :: Type -> Type) + --> 10:1..10:54 + | +10 | derive instance Bifoldable (WrapBothNoConstraint f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap index 70a1cc4f..f2afb618 100644 --- a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap +++ b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -30,5 +31,13 @@ Derived derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Traversable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 175..250: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) -error[NoInstanceFound] at 175..250: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound]: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) + --> 7:1..7:76 + | +7 | derive instance (Traversable f, Traversable g) => Traversable (Compose f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) + --> 7:1..7:76 + | +7 | derive instance (Traversable f, Traversable g) => Traversable (Compose f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index 16fe4582..8e16d36e 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -97,5 +98,13 @@ derive Eq1 (Rec :: Type -> Type) derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound] at 711..753: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 918..942: No instance found for: Eq (NoEq (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) + --> 35:1..35:43 + | +35 | derive instance Eq1 f => Eq1 (Compose f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Eq (NoEq (~&0 :: Type)) + --> 45:1..45:25 + | +45 | derive instance Eq1 NoEq + | ^~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index 1a2f4a4f..9de28c7c 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -68,6 +69,18 @@ derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: T derive Ord1 (&0 :: Type -> Type) => Ord1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound] at 665..707: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 708..752: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) -error[NoInstanceFound] at 878..904: No instance found for: Ord (NoOrd (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) + --> 26:1..26:43 + | +26 | derive instance Eq1 f => Eq1 (Compose f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) + --> 27:1..27:45 + | +27 | derive instance Ord1 f => Ord1 (Compose f g) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Ord (NoOrd (~&0 :: Type)) + --> 34:1..34:27 + | +34 | derive instance Ord1 NoOrd + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap index 01166a71..5a6910bf 100644 --- a/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking/171_derive_newtype_class_not_newtype/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,4 +19,8 @@ Roles NotANewtype = [] Diagnostics -error[ExpectedNewtype] at 92..129: Expected a newtype, got: NotANewtype +error[ExpectedNewtype]: Expected a newtype, got: NotANewtype + --> 7:1..7:38 + | +7 | derive instance Newtype NotANewtype _ + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap index af62b2c8..de35971f 100644 --- a/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap +++ b/tests-integration/fixtures/checking/181_role_declaration_loosen_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,4 +22,8 @@ Roles F = [Representational, Nominal] Diagnostics -error[InvalidRoleDeclaration] at 19..39: Invalid role declaration: declared Phantom, inferred Nominal +error[InvalidRoleDeclaration]: Invalid role declaration: declared Phantom, inferred Nominal + --> 3:1..3:21 + | +3 | data F f a = F (f a) + | ^~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap index 857f48c7..f754bfe8 100644 --- a/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap +++ b/tests-integration/fixtures/checking/189_coercible_different_heads_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -28,4 +29,8 @@ Maybe = [Representational] Either = [Representational, Representational] Diagnostics -error[NoInstanceFound] at 116..165: No instance found for: Coercible @Type (Maybe Int) (Either Int String) +error[NoInstanceFound]: No instance found for: Coercible @Type (Maybe Int) (Either Int String) + --> 8:1..8:50 + | +8 | coerceDifferent :: Maybe Int -> Either Int String + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap index a4d05fc9..63c70a84 100644 --- a/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap +++ b/tests-integration/fixtures/checking/190_coercible_nominal/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,4 +14,8 @@ Roles Nominal = [Nominal] Diagnostics -error[NoInstanceFound] at 196..251: No instance found for: Coercible @Type (Nominal Int) (Nominal String) +error[NoInstanceFound]: No instance found for: Coercible @Type (Nominal Int) (Nominal String) + --> 12:1..12:56 + | +12 | coerceNominalDifferent :: Nominal Int -> Nominal String + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap index 1a7711a4..f4536d2c 100644 --- a/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap +++ b/tests-integration/fixtures/checking/191_coercible_newtype_hidden/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,7 +10,23 @@ coerceQualified :: Int -> HiddenAge Types Diagnostics -error[CoercibleConstructorNotInScope] at 87..119: Constructor not in scope for Coercible -error[NoInstanceFound] at 87..119: No instance found for: Coercible @Type Int HiddenAge -error[CoercibleConstructorNotInScope] at 143..180: Constructor not in scope for Coercible -error[NoInstanceFound] at 143..180: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope]: Constructor not in scope for Coercible + --> 7:1..7:33 + | +7 | coerceHidden :: Int -> HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Coercible @Type Int HiddenAge + --> 7:1..7:33 + | +7 | coerceHidden :: Int -> HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CoercibleConstructorNotInScope]: Constructor not in scope for Coercible + --> 10:1..10:38 + | +10 | coerceQualified :: Int -> L.HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Coercible @Type Int HiddenAge + --> 10:1..10:38 + | +10 | coerceQualified :: Int -> L.HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap index 2b77a5be..e9fde3f1 100644 --- a/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap +++ b/tests-integration/fixtures/checking/193_coercible_newtype_open_hidden/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -8,5 +9,13 @@ coerceOpen :: Int -> HiddenAge Types Diagnostics -error[CoercibleConstructorNotInScope] at 59..89: Constructor not in scope for Coercible -error[NoInstanceFound] at 59..89: No instance found for: Coercible @Type Int HiddenAge +error[CoercibleConstructorNotInScope]: Constructor not in scope for Coercible + --> 6:1..6:31 + | +6 | coerceOpen :: Int -> HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Coercible @Type Int HiddenAge + --> 6:1..6:31 + | +6 | coerceOpen :: Int -> HiddenAge + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap index a60be456..7a4d589c 100644 --- a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap +++ b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -30,4 +31,8 @@ List = [Representational] Container = [Representational] Diagnostics -error[NoInstanceFound] at 211..272: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) + --> 11:1..11:62 + | +11 | coerceContainerDifferent :: Container Maybe -> Container List + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap index a9108932..1e95992b 100644 --- a/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap +++ b/tests-integration/fixtures/checking/202_int_compare_invalid/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -12,10 +13,38 @@ invalidEq :: Proxy @(Row Int) ( left :: 1, right :: 5 ) Types Diagnostics -error[NoInstanceFound] at 132..182: No instance found for: Compare 10 5 LT -error[NoInstanceFound] at 132..182: No instance found for: Compare 5 1 LT -error[NoInstanceFound] at 309..359: No instance found for: Compare 1 5 GT -error[NoInstanceFound] at 309..359: No instance found for: Compare 5 10 GT -error[NoInstanceFound] at 444..488: No instance found for: Compare 5 1 LT -error[NoInstanceFound] at 515..559: No instance found for: Compare 1 5 GT -error[NoInstanceFound] at 587..631: No instance found for: Compare 1 5 EQ +error[NoInstanceFound]: No instance found for: Compare 10 5 LT + --> 6:1..6:51 + | +6 | invalidTransLt :: Proxy ( left :: 10, right :: 1 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 5 1 LT + --> 6:1..6:51 + | +6 | invalidTransLt :: Proxy ( left :: 10, right :: 1 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 1 5 GT + --> 10:1..10:51 + | +10 | invalidTransGt :: Proxy ( left :: 1, right :: 10 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 5 10 GT + --> 10:1..10:51 + | +10 | invalidTransGt :: Proxy ( left :: 1, right :: 10 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 5 1 LT + --> 14:1..14:45 + | +14 | invalidLt :: Proxy ( left :: 5, right :: 1 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 1 5 GT + --> 17:1..17:45 + | +17 | invalidGt :: Proxy ( left :: 1, right :: 5 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[NoInstanceFound]: No instance found for: Compare 1 5 EQ + --> 20:1..20:45 + | +20 | invalidEq :: Proxy ( left :: 1, right :: 5 ) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap index 8d9c8ebe..ee4188a9 100644 --- a/tests-integration/fixtures/checking/205_builtin_warn/Main.snap +++ b/tests-integration/fixtures/checking/205_builtin_warn/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -48,12 +49,44 @@ Roles Proxy = [Phantom] Diagnostics -warning[CustomWarning] at 243..262: This function is deprecated -warning[CustomWarning] at 388..408: Left Right -warning[CustomWarning] at 535..554: Line 1 +warning[CustomWarning]: This function is deprecated + --> 11:1..11:20 + | +11 | useWarnBasic :: Int + | ^~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Left Right + --> 17:1..17:21 + | +17 | useWarnBeside :: Int + | ^~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Line 1 Line 2 -warning[CustomWarning] at 690..715: Got type: Int -warning[CustomWarning] at 862..886: Label: myField -warning[CustomWarning] at 1054..1084: Label: "h e l l o" -warning[CustomWarning] at 1260..1289: Label: "hel\"lo" -warning[CustomWarning] at 1467..1494: Label: """raw\nstring""" + --> 23:1..23:20 + | +23 | useWarnAbove :: Int + | ^~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Got type: Int + --> 29:1..29:26 + | +29 | useWarnQuote :: Proxy Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Label: myField + --> 35:1..35:25 + | +35 | useWarnQuoteLabel :: Int + | ^~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Label: "h e l l o" + --> 41:1..41:31 + | +41 | useWarnQuoteLabelSpaces :: Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Label: "hel\"lo" + --> 47:1..47:30 + | +47 | useWarnQuoteLabelQuote :: Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Label: """raw\nstring""" + --> 53:1..53:28 + | +53 | useWarnQuoteLabelRaw :: Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap index 5da56926..25ec6ed1 100644 --- a/tests-integration/fixtures/checking/206_builtin_fail/Main.snap +++ b/tests-integration/fixtures/checking/206_builtin_fail/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -27,6 +28,14 @@ Roles Proxy = [Phantom] Diagnostics -error[CustomFailure] at 233..252: This operation is not allowed -error[CustomFailure] at 411..441: Error: +error[CustomFailure]: This operation is not allowed + --> 11:1..11:20 + | +11 | useFailBasic :: Int + | ^~~~~~~~~~~~~~~~~~~ +error[CustomFailure]: Error: Type String + --> 17:1..17:31 + | +17 | useFailComplex :: Proxy String + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/215_do_bind_error/Main.snap b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap index 279af6da..681f2fe2 100644 --- a/tests-integration/fixtures/checking/215_do_bind_error/Main.snap +++ b/tests-integration/fixtures/checking/215_do_bind_error/Main.snap @@ -21,4 +21,8 @@ Roles Effect = [Nominal] Diagnostics -error[CannotUnify] at 385..386: Cannot unify 'String' with 'Int' +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 14:15..14:16 + | +14 | pure (add a b) + | ^ diff --git a/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap b/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap index bb13f0e2..1db43b1f 100644 --- a/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap +++ b/tests-integration/fixtures/checking/216_ado_bind_error/Main.snap @@ -21,4 +21,8 @@ Roles Effect = [Nominal] Diagnostics -error[CannotUnify] at 354..355: Cannot unify 'String' with 'Int' +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 13:12..13:13 + | +13 | in add a b + | ^ diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 2c02a32c..546211de 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -23,5 +23,13 @@ Effect = [Nominal] Aff = [Nominal] Diagnostics -error[CannotUnify] at 439..453: Cannot unify 'Aff' with 'Effect' -error[CannotUnify] at 439..453: Cannot unify 'Aff String' with 'Effect ?15[:0]' +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 15:8..15:22 + | +15 | b <- affPure "life" + | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' + --> 15:8..15:22 + | +15 | b <- affPure "life" + | ^~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap index c9b78f7c..cb498d02 100644 --- a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap @@ -23,5 +23,13 @@ Effect = [Nominal] Aff = [Nominal] Diagnostics -error[CannotUnify] at 430..444: Cannot unify 'Aff' with 'Effect' -error[CannotUnify] at 430..444: Cannot unify 'Aff String' with 'Effect ?15[:0]' +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 15:8..15:22 + | +15 | b <- affPure "life" + | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' + --> 15:8..15:22 + | +15 | b <- affPure "life" + | ^~~~~~~~~~~~~~ diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index ef2f7142..120358a7 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; -use diagnostics::{DiagnosticsContext, ToDiagnostics, format_text}; +use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; use indexing::{ImportKind, TermItem, TypeItem, TypeItemId, TypeItemKind}; use lowering::{ @@ -414,7 +414,7 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { if !all_diagnostics.is_empty() { writeln!(snapshot, "\nDiagnostics").unwrap(); - snapshot.push_str(&format_text(&all_diagnostics)); + snapshot.push_str(&format_rustc(&all_diagnostics, &content)); } snapshot From 41e5974acfcdd4d5664b73741e0cf3135437978c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 03:36:01 +0800 Subject: [PATCH 022/386] Add inference tests to 217 and 218 --- .../fixtures/checking/217_do_monad_error/Main.purs | 6 ++++++ .../fixtures/checking/217_do_monad_error/Main.snap | 11 +++++++++++ .../fixtures/checking/218_ado_monad_error/Main.purs | 6 ++++++ .../fixtures/checking/218_ado_monad_error/Main.snap | 11 +++++++++++ 4 files changed, 34 insertions(+) diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.purs b/tests-integration/fixtures/checking/217_do_monad_error/Main.purs index 84093e47..7307e2ad 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.purs +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.purs @@ -16,3 +16,9 @@ test = do c <- pure 123456 pure { a, b, c } +test' = do + a <- pure 123456 + b <- affPure "life" + c <- pure 123456 + pure { a, b, c } + diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 546211de..7544d0a3 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -13,6 +13,7 @@ discard :: forall (a :: Type) (b :: Type). Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } +test' :: forall (t24 :: Type). Effect { a :: Int, b :: (t24 :: Type), c :: Int } Types Effect :: Type -> Type @@ -33,3 +34,13 @@ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' | 15 | b <- affPure "life" | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 21:8..21:22 + | +21 | b <- affPure "life" + | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?31[:0]' + --> 21:8..21:22 + | +21 | b <- affPure "life" + | ^~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs b/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs index 7e293ab9..f5794d64 100644 --- a/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.purs @@ -15,3 +15,9 @@ test = ado b <- affPure "life" c <- pure 123456 in { a, b, c } + +test' = ado + a <- pure 123456 + b <- affPure "life" + c <- pure 123456 + in { a, b, c } diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap index cb498d02..931563fe 100644 --- a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap @@ -13,6 +13,7 @@ apply :: forall (a :: Type) (b :: Type). Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } +test' :: forall (t30 :: Type). Effect { a :: Int, b :: (t30 :: Type), c :: Int } Types Effect :: Type -> Type @@ -33,3 +34,13 @@ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' | 15 | b <- affPure "life" | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 21:8..21:22 + | +21 | b <- affPure "life" + | ^~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?30[:0]' + --> 21:8..21:22 + | +21 | b <- affPure "life" + | ^~~~~~~~~~~~~~ From cd60cfcf0568ffa3fa71d51dcddcbb816f7606d8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 16:43:19 +0800 Subject: [PATCH 023/386] Add more modules to prelude --- .../checking/prelude/Control.Bind.purs | 9 ++++++ .../checking/prelude/Control.Monad.purs | 6 ++++ .../fixtures/checking/prelude/Effect.Aff.purs | 28 +++++++++++++++++++ .../fixtures/checking/prelude/Effect.purs | 28 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 tests-integration/fixtures/checking/prelude/Control.Bind.purs create mode 100644 tests-integration/fixtures/checking/prelude/Control.Monad.purs create mode 100644 tests-integration/fixtures/checking/prelude/Effect.Aff.purs create mode 100644 tests-integration/fixtures/checking/prelude/Effect.purs diff --git a/tests-integration/fixtures/checking/prelude/Control.Bind.purs b/tests-integration/fixtures/checking/prelude/Control.Bind.purs new file mode 100644 index 00000000..0ee7419f --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Control.Bind.purs @@ -0,0 +1,9 @@ +module Control.Bind where + +import Control.Apply (class Apply) + +class Apply m <= Bind m where + bind :: forall a b. m a -> (a -> m b) -> m b + +discard :: forall a b m. Bind m => m a -> (a -> m b) -> m b +discard = bind diff --git a/tests-integration/fixtures/checking/prelude/Control.Monad.purs b/tests-integration/fixtures/checking/prelude/Control.Monad.purs new file mode 100644 index 00000000..99eedb05 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Control.Monad.purs @@ -0,0 +1,6 @@ +module Control.Monad where + +import Control.Applicative (class Applicative) +import Control.Bind (class Bind) + +class (Applicative m, Bind m) <= Monad m diff --git a/tests-integration/fixtures/checking/prelude/Effect.Aff.purs b/tests-integration/fixtures/checking/prelude/Effect.Aff.purs new file mode 100644 index 00000000..4fef90e6 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Effect.Aff.purs @@ -0,0 +1,28 @@ +module Effect.Aff where + +import Control.Applicative (class Applicative) +import Control.Apply (class Apply) +import Control.Bind (class Bind) +import Control.Monad (class Monad) +import Data.Functor (class Functor) + +foreign import data Aff :: Type -> Type + +foreign import mapAff :: forall a b. (a -> b) -> Aff a -> Aff b +foreign import applyAff :: forall a b. Aff (a -> b) -> Aff a -> Aff b +foreign import pureAff :: forall a. a -> Aff a +foreign import bindAff :: forall a b. Aff a -> (a -> Aff b) -> Aff b + +instance Functor Aff where + map = mapAff + +instance Apply Aff where + apply = applyAff + +instance Applicative Aff where + pure = pureAff + +instance Bind Aff where + bind = bindAff + +instance Monad Aff diff --git a/tests-integration/fixtures/checking/prelude/Effect.purs b/tests-integration/fixtures/checking/prelude/Effect.purs new file mode 100644 index 00000000..f634e9ea --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Effect.purs @@ -0,0 +1,28 @@ +module Effect where + +import Control.Applicative (class Applicative) +import Control.Apply (class Apply) +import Control.Bind (class Bind) +import Control.Monad (class Monad) +import Data.Functor (class Functor) + +foreign import data Effect :: Type -> Type + +foreign import mapEffect :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import applyEffect :: forall a b. Effect (a -> b) -> Effect a -> Effect b +foreign import pureEffect :: forall a. a -> Effect a +foreign import bindEffect :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +instance Functor Effect where + map = mapEffect + +instance Apply Effect where + apply = applyEffect + +instance Applicative Effect where + pure = pureEffect + +instance Bind Effect where + bind = bindEffect + +instance Monad Effect From 8547116df21e0fe090cccefb240dd0e9652ddeb0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 17:16:32 +0800 Subject: [PATCH 024/386] Initial experiment with left to right checking --- compiler-core/checking/src/algorithm/term.rs | 169 +++++++----------- .../checking/053_do_polymorphic/Main.snap | 2 +- .../checking/117_do_ado_constrained/Main.snap | 10 +- .../checking/217_do_monad_error/Main.snap | 6 +- .../219_do_mixed_monad_error/Main.purs | 23 +++ .../219_do_mixed_monad_error/Main.snap | 34 ++++ tests-integration/tests/checking/generated.rs | 2 + 7 files changed, 134 insertions(+), 112 deletions(-) create mode 100644 tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.purs create mode 100644 tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 04d26e3b..f43f16a6 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -652,80 +652,82 @@ where return Ok(context.prim.unknown); }; - // Create a fresh unification variable for the pure_expression. - // Inferring pure_expression directly may solve the unification - // variables introduced in the first pass. This is undesirable, - // because the errors would be attributed incorrectly to the - // do statements rather than the pure_expression itself. + // Create suffix metavariables for each statement boundary. S[i] represents + // the type of the do-block suffix starting at statement i. // - // main = do - // pure 42 - // y <- pure "Hello!" - // pure $ Message y + // For a do-block with statements [s0, s1, s2, final]: + // S[0] = type of whole block (starting at s0) + // S[1] = type of suffix starting at s1 + // S[2] = type of suffix starting at s2 + // S[3] = type of suffix starting at final // - // pure_expression :: Effect Message - // pure_expression_type := ?pure_expression - let pure_expression_type = state.fresh_unification_type(context); - - // The desugard form of a do-expression is checked inside out. An - // expression like `bind m_a (\a -> m_b)` is checked by inferring - // the type of the continuation before the `bind` application can - // be checked. In the example we have above, we have the following: + // Each bind/discard statement constrains its suffix type based on the + // next suffix type, allowing errors to be attributed to the first + // conflicting statement when processed left-to-right. + let suffix_types: Vec<_> = + (0..=bind_statements.len()).map(|_| state.fresh_unification_type(context)).collect(); + + // Process statements left-to-right. Each statement: + // 1. Infers the expression type + // 2. Applies bind/discard with the next suffix type as continuation + // 3. Unifies the result with the current suffix type // - // bind (pure "Hello!") (\y -> pure $ Message y) + // Example with Effect and Aff mixed: + // do + // a <- effect -- Effect Int + // b <- aff -- Aff Int (conflict!) + // pure { a, b } // - // discard (pure 42) (\_ -> ) + // Statement 0 (a <- effect): + // bind :: m a -> (a -> m b) -> m b + // Instantiate: Effect Int -> (Int -> Effect ?b0) -> Effect ?b0 + // Continuation uses S[1], so S[1] ~ Effect ?b0 + // Result S[0] ~ Effect ?b0 // - // To emulate this, the infer_do_bind rule applies `bind` to the - // inferred expression type and a function type synthesised using - // the unification variables we created previously. - // - // expression_type := Effect String - // - // lambda_type := ?y -> continuation_type - // := ?y -> ?pure_expression - // - // bind_type := m a -> (a -> m b) -> m b - // apply(expression) := (String -> Effect ?b) -> Effect ?b - // apply(lambda) := Effect ?b - // >> - // >> ?y := String - // >> ?pure_expression := Effect ?b - // - // continuation_type := Effect ?b - // - // Then, the infer_do_discard rule applies `discard` to the - // inferred expression type and extracts the result type. - // - // expression_type := Effect Int - // - // discard_type := m a -> (a -> m b) -> m b - // apply(expression) := (Int -> Effect ?b) -> Effect ?b - // extract(lambda) := Effect ?b - // - // continuation_type := Effect ?b - let mut continuation_type = pure_expression_type; - for (binder, expression) in bind_statements.iter().rev() { - continuation_type = if let Some(binder) = binder { - infer_do_bind(state, context, bind_type, continuation_type, *expression, *binder)? + // Statement 1 (b <- aff): + // bind :: m a -> (a -> m b) -> m b + // Instantiate: Aff Int -> (Int -> Aff ?b1) -> Aff ?b1 + // Continuation uses S[2], result must match S[1] + // But S[1] = Effect ?b0, and result = Aff ?b1 + // CONFLICT at statement 1 (aff line) - correct attribution! + for (i, (binder, expression)) in bind_statements.iter().enumerate() { + let current_suffix = suffix_types[i]; + let next_suffix = suffix_types[i + 1]; + + let Some(expression) = *expression else { + continue; + }; + + // Wrap both the bind/discard inference and the suffix unification + // with an error step so that errors are attributed to the expression. + if let Some(binder) = binder { + state.with_error_step(ErrorStep::InferringDoBind(expression), |state| { + let statement_type = + infer_do_bind(state, context, bind_type, next_suffix, expression, *binder)?; + // Unify the statement result with the current suffix type. + // This is where conflicts are detected, blamed on current statement. + let _ = unification::subtype(state, context, statement_type, current_suffix)?; + Ok(()) + })?; } else { - infer_do_discard(state, context, discard_type, continuation_type, *expression)? + state.with_error_step(ErrorStep::InferringDoDiscard(expression), |state| { + let statement_type = + infer_do_discard(state, context, discard_type, next_suffix, expression)?; + let _ = unification::subtype(state, context, statement_type, current_suffix)?; + Ok(()) + })?; }; } - // Finally, check the pure expression against the pure_expression_type. - // At this point we're confident that the unification variables created - // for the do statement bindings have been solved to concrete types. - // By performing a check against a unification variable instead of - // performing inference on pure_expression early, we get better errors. - // - // pure_expression :: Effect Message - // continuation_type := Effect ?b - // >> - // >> ?b := Message - let _ = check_expression(state, context, *pure_expression, pure_expression_type)?; + // The final suffix type is the type of the pure expression. + let pure_expression_suffix = *suffix_types.last().unwrap(); - Ok(continuation_type) + // Check the pure expression against its suffix type. + // This constrains the final suffix and propagates back through unification. + let _ = check_expression(state, context, *pure_expression, pure_expression_suffix)?; + + // The whole block's type is S[0]. + Ok(suffix_types[0]) } #[tracing::instrument(skip_all, name = "infer_ado")] @@ -1186,26 +1188,6 @@ where #[tracing::instrument(skip_all, name = "infer_do_bind")] fn infer_do_bind( - state: &mut CheckState, - context: &CheckContext, - bind_type: TypeId, - continuation_type: TypeId, - expression: Option, - binder_type: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let Some(expression) = expression else { - return Ok(context.prim.unknown); - }; - - state.with_error_step(ErrorStep::InferringDoBind(expression), |state| { - infer_do_bind_core(state, context, bind_type, continuation_type, expression, binder_type) - }) -} - -fn infer_do_bind_core( state: &mut CheckState, context: &CheckContext, bind_type: TypeId, @@ -1251,25 +1233,6 @@ where #[tracing::instrument(skip_all, name = "infer_do_discard")] fn infer_do_discard( - state: &mut CheckState, - context: &CheckContext, - discard_type: TypeId, - continuation_type: TypeId, - expression: Option, -) -> QueryResult -where - Q: ExternalQueries, -{ - let Some(expression) = expression else { - return Ok(context.prim.unknown); - }; - - state.with_error_step(ErrorStep::InferringDoDiscard(expression), |state| { - infer_do_discard_core(state, context, discard_type, continuation_type, expression) - }) -} - -fn infer_do_discard_core( state: &mut CheckState, context: &CheckContext, discard_type: TypeId, diff --git a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap index 919bd3de..9de9d422 100644 --- a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap @@ -17,7 +17,7 @@ discard :: (m :: Type -> Type) (b :: Type) pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) test :: forall (m :: Type -> Type). (m :: Type -> Type) (Tuple Int String) -test' :: forall (t56 :: Type -> Type). (t56 :: Type -> Type) (Tuple Int String) +test' :: forall (t55 :: Type -> Type). (t55 :: Type -> Type) (Tuple Int String) Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index f4dd61f9..16e6a030 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -35,17 +35,17 @@ bind :: testDo :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) (Tuple Int String) testDo' :: - forall (t53 :: Type -> Type). - Bind (t53 :: Type -> Type) => (t53 :: Type -> Type) (Tuple Int String) + forall (t52 :: Type -> Type). + Bind (t52 :: Type -> Type) => (t52 :: Type -> Type) (Tuple Int String) testAdo :: forall (f :: Type -> Type). Applicative (f :: Type -> Type) => (f :: Type -> Type) (Tuple Int String) testAdo' :: - forall (t87 :: Type -> Type). - Applicative (t87 :: Type -> Type) => (t87 :: Type -> Type) (Tuple Int String) + forall (t91 :: Type -> Type). + Applicative (t91 :: Type -> Type) => (t91 :: Type -> Type) (Tuple Int String) testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int testDoDiscard' :: - forall (t105 :: Type -> Type). Discard (t105 :: Type -> Type) => (t105 :: Type -> Type) Int + forall (t111 :: Type -> Type). Discard (t111 :: Type -> Type) => (t111 :: Type -> Type) Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 7544d0a3..6ac79e0f 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -13,7 +13,7 @@ discard :: forall (a :: Type) (b :: Type). Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } -test' :: forall (t24 :: Type). Effect { a :: Int, b :: (t24 :: Type), c :: Int } +test' :: forall (t27 :: Type). Effect { a :: Int, b :: (t27 :: Type), c :: Int } Types Effect :: Type -> Type @@ -29,7 +29,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 15 | b <- affPure "life" | ^~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?18[:0]' --> 15:8..15:22 | 15 | b <- affPure "life" @@ -39,7 +39,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 21 | b <- affPure "life" | ^~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?31[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?37[:0]' --> 21:8..21:22 | 21 | b <- affPure "life" diff --git a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.purs b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.purs new file mode 100644 index 00000000..b93bca14 --- /dev/null +++ b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.purs @@ -0,0 +1,23 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind, discard) +import Effect (Effect) +import Effect.Aff (Aff) + +foreign import effect :: Effect Int +foreign import aff :: Aff String + +-- Test: error should be attributed to the `aff` line (first conflicting statement) +-- not the `effect` line +test :: Effect { a :: Int, b :: String } +test = do + a <- effect + b <- aff + pure { a, b } + +-- Inference variant +test' = do + a <- effect + b <- aff + pure { a, b } diff --git a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap new file mode 100644 index 00000000..9cdc6e8c --- /dev/null +++ b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +effect :: Effect Int +aff :: Aff String +test :: Effect { a :: Int, b :: String } +test' :: forall (t23 :: Type). Effect (t23 :: Type) + +Types + +Diagnostics +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 16:8..16:11 + | +16 | b <- aff + | ^~~ +error[CannotUnify]: Cannot unify 'Aff ?11[:0]' with 'Effect ?8[:0]' + --> 16:8..16:11 + | +16 | b <- aff + | ^~~ +error[CannotUnify]: Cannot unify 'Aff' with 'Effect' + --> 22:8..22:11 + | +22 | b <- aff + | ^~~ +error[CannotUnify]: Cannot unify 'Aff ?26[:0]' with 'Effect ?23[:0]' + --> 22:8..22:11 + | +22 | b <- aff + | ^~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index b4633fbd..3d8ca0d0 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -463,3 +463,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_217_do_monad_error_main() { run_test("217_do_monad_error", "Main"); } #[rustfmt::skip] #[test] fn test_218_ado_monad_error_main() { run_test("218_ado_monad_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_219_do_mixed_monad_error_main() { run_test("219_do_mixed_monad_error", "Main"); } From 575b09be5aa21be535b863da5bc83c241c45702f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 23 Jan 2026 22:07:24 +0800 Subject: [PATCH 025/386] Use DoStatementId for InferringDoBind and InferringDoDiscard --- compiler-core/checking/src/algorithm/term.rs | 28 +++++++++++-------- compiler-core/checking/src/error.rs | 4 +-- compiler-core/lowering/src/algorithm.rs | 4 +++ .../lowering/src/algorithm/recursive.rs | 13 +++++++-- compiler-core/lowering/src/intermediate.rs | 13 +++++++-- .../checking/217_do_monad_error/Main.snap | 16 +++++------ .../219_do_mixed_monad_error/Main.snap | 16 +++++------ 7 files changed, 60 insertions(+), 34 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index f43f16a6..ac48021f 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -612,7 +612,7 @@ fn infer_do( context: &CheckContext, bind: lowering::TermVariableResolution, discard: lowering::TermVariableResolution, - statements: &[lowering::DoStatement], + statement_ids: &[lowering::DoStatementId], ) -> QueryResult where Q: ExternalQueries, @@ -624,7 +624,10 @@ where // bound to unification variables and let bindings are checked. // This is like inferring the lambda in a desugared representation. let mut do_statements = vec![]; - for statement in statements.iter() { + for &statement_id in statement_ids.iter() { + let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { + continue; + }; match statement { lowering::DoStatement::Bind { binder, expression } => { let binder_type = if let Some(binder) = binder { @@ -632,18 +635,18 @@ where } else { state.fresh_unification_type(context) }; - do_statements.push((Some(binder_type), *expression)); + do_statements.push((statement_id, Some(binder_type), *expression)); } lowering::DoStatement::Let { statements } => { check_let_chunks(state, context, statements)?; } lowering::DoStatement::Discard { expression } => { - do_statements.push((None, *expression)); + do_statements.push((statement_id, None, *expression)); } } } - let [bind_statements @ .., (_, pure_expression)] = &do_statements[..] else { + let [bind_statements @ .., (_, _, pure_expression)] = &do_statements[..] else { state.insert_error(ErrorKind::EmptyDoBlock); return Ok(context.prim.unknown); }; @@ -690,7 +693,7 @@ where // Continuation uses S[2], result must match S[1] // But S[1] = Effect ?b0, and result = Aff ?b1 // CONFLICT at statement 1 (aff line) - correct attribution! - for (i, (binder, expression)) in bind_statements.iter().enumerate() { + for (i, (statement, binder, expression)) in bind_statements.iter().enumerate() { let current_suffix = suffix_types[i]; let next_suffix = suffix_types[i + 1]; @@ -699,9 +702,9 @@ where }; // Wrap both the bind/discard inference and the suffix unification - // with an error step so that errors are attributed to the expression. + // with an error step so that errors are attributed to the full statement. if let Some(binder) = binder { - state.with_error_step(ErrorStep::InferringDoBind(expression), |state| { + state.with_error_step(ErrorStep::InferringDoBind(*statement), |state| { let statement_type = infer_do_bind(state, context, bind_type, next_suffix, expression, *binder)?; // Unify the statement result with the current suffix type. @@ -710,7 +713,7 @@ where Ok(()) })?; } else { - state.with_error_step(ErrorStep::InferringDoDiscard(expression), |state| { + state.with_error_step(ErrorStep::InferringDoDiscard(*statement), |state| { let statement_type = infer_do_discard(state, context, discard_type, next_suffix, expression)?; let _ = unification::subtype(state, context, statement_type, current_suffix)?; @@ -737,7 +740,7 @@ fn infer_ado( map: lowering::TermVariableResolution, apply: lowering::TermVariableResolution, pure: lowering::TermVariableResolution, - statements: &[lowering::DoStatement], + statement_ids: &[lowering::DoStatementId], expression: Option, ) -> QueryResult where @@ -752,7 +755,10 @@ where // This is like inferring the lambda in a desugared representation. let mut binder_types = vec![]; let mut ado_statements = vec![]; - for statement in statements.iter() { + for &statement_id in statement_ids.iter() { + let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { + continue; + }; match statement { lowering::DoStatement::Bind { binder, expression } => { let binder_type = if let Some(binder) = binder { diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 4f933bed..4fecf2d8 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -23,8 +23,8 @@ pub enum ErrorStep { InferringExpression(lowering::ExpressionId), CheckingExpression(lowering::ExpressionId), - InferringDoBind(lowering::ExpressionId), - InferringDoDiscard(lowering::ExpressionId), + InferringDoBind(lowering::DoStatementId), + InferringDoDiscard(lowering::DoStatementId), InferringAdoMap(lowering::ExpressionId), InferringAdoApply(lowering::ExpressionId), diff --git a/compiler-core/lowering/src/algorithm.rs b/compiler-core/lowering/src/algorithm.rs index 780d2bd0..c0235b3d 100644 --- a/compiler-core/lowering/src/algorithm.rs +++ b/compiler-core/lowering/src/algorithm.rs @@ -114,6 +114,10 @@ impl State { self.nodes.type_node.insert(id, node); } + fn associate_do_statement(&mut self, id: DoStatementId, statement: DoStatement) { + self.info.do_statement.insert(id, statement); + } + fn associate_let_binding_name(&mut self, id: LetBindingNameGroupId, info: LetBindingName) { self.info.let_binding_name.insert(id, info); let Some(node) = self.graph_scope else { return }; diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index 0daaa43f..e5231086 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -725,8 +725,13 @@ pub(crate) fn lower_equation_like( }) } -fn lower_do_statement(state: &mut State, context: &Context, cst: &cst::DoStatement) -> DoStatement { - match cst { +fn lower_do_statement( + state: &mut State, + context: &Context, + cst: &cst::DoStatement, +) -> DoStatementId { + let id = context.stabilized.lookup_cst(cst).expect_id(); + let statement = match cst { cst::DoStatement::DoStatementBind(cst) => { let expression = cst.expression().map(|cst| lower_expression(state, context, &cst)); state.push_binder_scope(); @@ -741,7 +746,9 @@ fn lower_do_statement(state: &mut State, context: &Context, cst: &cst::DoStateme let expression = cst.expression().map(|cst| lower_expression(state, context, &cst)); DoStatement::Discard { expression } } - } + }; + state.associate_do_statement(id, statement); + id } fn lower_record_updates( diff --git a/compiler-core/lowering/src/intermediate.rs b/compiler-core/lowering/src/intermediate.rs index 21c73438..7e136d0c 100644 --- a/compiler-core/lowering/src/intermediate.rs +++ b/compiler-core/lowering/src/intermediate.rs @@ -113,13 +113,13 @@ pub enum ExpressionKind { Do { bind: Option, discard: Option, - statements: Arc<[DoStatement]>, + statements: Arc<[DoStatementId]>, }, Ado { map: Option, apply: Option, pure: Option, - statements: Arc<[DoStatement]>, + statements: Arc<[DoStatementId]>, expression: Option, }, Constructor { @@ -418,6 +418,7 @@ pub struct LoweringInfo { pub(crate) term_item: FxHashMap, pub(crate) type_item: FxHashMap, + pub(crate) do_statement: FxHashMap, pub(crate) let_binding: Arena, pub(crate) let_binding_name: ArenaMap, @@ -438,6 +439,10 @@ impl LoweringInfo { self.type_kind.iter().map(|(k, v)| (*k, v)) } + pub fn iter_do_statement(&self) -> impl Iterator { + self.do_statement.iter().map(|(k, v)| (*k, v)) + } + pub fn iter_term_operator(&self) -> impl Iterator { self.term_operator.iter().map(|(o_id, (f_id, t_id))| (*o_id, *f_id, *t_id)) } @@ -458,6 +463,10 @@ impl LoweringInfo { self.type_kind.get(&id) } + pub fn get_do_statement(&self, id: DoStatementId) -> Option<&DoStatement> { + self.do_statement.get(&id) + } + pub fn get_term_item(&self, id: TermItemId) -> Option<&TermItemIr> { self.term_item.get(&id) } diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 6ac79e0f..252b65fa 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -25,22 +25,22 @@ Aff = [Nominal] Diagnostics error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 15:8..15:22 + --> 15:3..15:22 | 15 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?18[:0]' - --> 15:8..15:22 + --> 15:3..15:22 | 15 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 21:8..21:22 + --> 21:3..21:22 | 21 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?37[:0]' - --> 21:8..21:22 + --> 21:3..21:22 | 21 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap index 9cdc6e8c..e1290c6f 100644 --- a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap @@ -13,22 +13,22 @@ Types Diagnostics error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 16:8..16:11 + --> 16:3..16:11 | 16 | b <- aff - | ^~~ + | ^~~~~~~~ error[CannotUnify]: Cannot unify 'Aff ?11[:0]' with 'Effect ?8[:0]' - --> 16:8..16:11 + --> 16:3..16:11 | 16 | b <- aff - | ^~~ + | ^~~~~~~~ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 22:8..22:11 + --> 22:3..22:11 | 22 | b <- aff - | ^~~ + | ^~~~~~~~ error[CannotUnify]: Cannot unify 'Aff ?26[:0]' with 'Effect ?23[:0]' - --> 22:8..22:11 + --> 22:3..22:11 | 22 | b <- aff - | ^~~ + | ^~~~~~~~ From 0897491beb54602eb346516bc17ec4c2d7f5925f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 05:45:56 +0800 Subject: [PATCH 026/386] Update do statement implementation and documentation --- compiler-core/checking/src/algorithm/term.rs | 200 ++++++++++++++----- 1 file changed, 154 insertions(+), 46 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index ac48021f..128f4ce0 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1,3 +1,5 @@ +use std::iter; + use building_types::QueryResult; use indexing::TermItemId; use itertools::Itertools; @@ -646,7 +648,7 @@ where } } - let [bind_statements @ .., (_, _, pure_expression)] = &do_statements[..] else { + let [binding_statements @ .., (_, _, pure_expression)] = &do_statements[..] else { state.insert_error(ErrorKind::EmptyDoBlock); return Ok(context.prim.unknown); }; @@ -655,20 +657,15 @@ where return Ok(context.prim.unknown); }; - // Create suffix metavariables for each statement boundary. S[i] represents - // the type of the do-block suffix starting at statement i. - // - // For a do-block with statements [s0, s1, s2, final]: - // S[0] = type of whole block (starting at s0) - // S[1] = type of suffix starting at s1 - // S[2] = type of suffix starting at s2 - // S[3] = type of suffix starting at final - // - // Each bind/discard statement constrains its suffix type based on the - // next suffix type, allowing errors to be attributed to the first - // conflicting statement when processed left-to-right. - let suffix_types: Vec<_> = - (0..=bind_statements.len()).map(|_| state.fresh_unification_type(context)).collect(); + // Create unification variables that each statement in the do expression + // will unify against. The next section will get into more detail how + // these are used. These unification variables are used to enable GHC-like + // left-to-right checking of ado-expression while maintaining the same + // semantics as rebindable `do` in PureScript. + let continuation_count = do_statements.len(); + let continuation_types = iter::repeat_with(|| state.fresh_unification_type(context)) + .take(continuation_count) + .collect_vec(); // Process statements left-to-right. Each statement: // 1. Infers the expression type @@ -693,44 +690,104 @@ where // Continuation uses S[2], result must match S[1] // But S[1] = Effect ?b0, and result = Aff ?b1 // CONFLICT at statement 1 (aff line) - correct attribution! - for (i, (statement, binder, expression)) in bind_statements.iter().enumerate() { - let current_suffix = suffix_types[i]; - let next_suffix = suffix_types[i + 1]; + // - let Some(expression) = *expression else { + let statements = binding_statements.iter().copied(); + let continuations = continuation_types.iter().tuple_windows::<(_, _)>(); + + // Iterate over each of the statements and a tuple window over the + // continuation types. Let's trace over the following example: + // + // main = do + // a <- effect + // b <- aff + // pure { a, b } + // + // For the first statement, we know the following information. We + // instantiate the `bind` function to prepare it for application. + // The first argument is easy, it's just the expression_type; the + // second argument involves synthesising a function type using the + // `binder_type` and the `next` continuation. The application of + // these arguments creates important unifications, listed below. + // Additionally, we also create a unification to unify the `now` + // type with the result of the `bind` application. + // + // expression_type := Effect Int + // binder_type := ?a + // now := ?0 + // next := ?1 + // lambda_type := ?a -> ?1 + // + // bind_type := m a -> (a -> m b) -> m b + // | + // := apply(expression_type) + // := (Int -> Effect ?r1) -> Effect ?r1 + // | + // := apply(lambda_type) + // := Effect ?r1 + // | + // >> ?a := Int + // >> ?1 := Effect ?r1 + // >> ?0 := Effect ?r1 + // + // For the second statement, we know the following information. + // The `now` type was already solved by the previous statement, + // + // expression_type := Aff Int + // binder_type := ?b + // now := ?1 := Effect ?r1 + // next := ?2 + // lambda_type := ?b -> ?2 + // + // bind_type := m a -> (a -> m b) -> m b + // | + // := apply(expression_type) + // := (Int -> Aff ?r2) -> Aff ?r2 + // | + // := apply(lambda_type) + // := Aff ?r2 + // | + // >> ?b := Int + // >> ?2 := Aff ?r2 + // | + // >> ?1 ~ Aff ?r2 + // >> Effect ?r1 ~ Aff ?r2 + // | + // >> Oh no! + // + // This unification error is expected, but this left-to-right checking + // approach significantly improves the reported error positions compared + // to the previous approach that emulated desugared checking. + for ((statement, binder, expression), (&now_type, &next_type)) in statements.zip(continuations) + { + let Some(expression) = expression else { continue; }; - - // Wrap both the bind/discard inference and the suffix unification - // with an error step so that errors are attributed to the full statement. - if let Some(binder) = binder { - state.with_error_step(ErrorStep::InferringDoBind(*statement), |state| { - let statement_type = - infer_do_bind(state, context, bind_type, next_suffix, expression, *binder)?; - // Unify the statement result with the current suffix type. - // This is where conflicts are detected, blamed on current statement. - let _ = unification::subtype(state, context, statement_type, current_suffix)?; - Ok(()) - })?; + if let Some(binder_type) = binder { + infer_do_bind( + state, + context, + InferDoBind { statement, bind_type, now_type, next_type, expression, binder_type }, + )?; } else { - state.with_error_step(ErrorStep::InferringDoDiscard(*statement), |state| { - let statement_type = - infer_do_discard(state, context, discard_type, next_suffix, expression)?; - let _ = unification::subtype(state, context, statement_type, current_suffix)?; - Ok(()) - })?; - }; + infer_do_discard( + state, + context, + InferDoDiscard { statement, discard_type, now_type, next_type, expression }, + )?; + } } - // The final suffix type is the type of the pure expression. - let pure_expression_suffix = *suffix_types.last().unwrap(); + // The `first_suffix` is the overall type of the do expression, built + // iteratively and through solving unification variables. Meanwhile, + // the `final_suffix` is the expected type for the final expression. + let [first_suffix, .., final_suffix] = continuation_types[..] else { + unreachable!("invariant violated: insufficient suffix_types"); + }; - // Check the pure expression against its suffix type. - // This constrains the final suffix and propagates back through unification. - let _ = check_expression(state, context, *pure_expression, pure_expression_suffix)?; + check_expression(state, context, *pure_expression, final_suffix)?; - // The whole block's type is S[0]. - Ok(suffix_types[0]) + Ok(first_suffix) } #[tracing::instrument(skip_all, name = "infer_ado")] @@ -795,7 +852,6 @@ where }; }; - // // Create a fresh unification variable for the in_expression. // Inferring expression directly may solve the unification variables // introduced in the first pass. This is undesirable, because the @@ -1192,8 +1248,35 @@ where ) } +struct InferDoBind { + statement: lowering::DoStatementId, + bind_type: TypeId, + now_type: TypeId, + next_type: TypeId, + expression: lowering::ExpressionId, + binder_type: TypeId, +} + #[tracing::instrument(skip_all, name = "infer_do_bind")] fn infer_do_bind( + state: &mut CheckState, + context: &CheckContext, + arguments: InferDoBind, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let InferDoBind { statement, bind_type, now_type, next_type, expression, binder_type } = + arguments; + state.with_error_step(ErrorStep::InferringDoBind(statement), move |state| { + let statement_type = + infer_do_bind_core(state, context, bind_type, next_type, expression, binder_type)?; + let _ = unification::subtype(state, context, statement_type, now_type)?; + Ok(()) + }) +} + +fn infer_do_bind_core( state: &mut CheckState, context: &CheckContext, bind_type: TypeId, @@ -1237,8 +1320,33 @@ where ) } +struct InferDoDiscard { + statement: lowering::DoStatementId, + discard_type: TypeId, + now_type: TypeId, + next_type: TypeId, + expression: lowering::ExpressionId, +} + #[tracing::instrument(skip_all, name = "infer_do_discard")] fn infer_do_discard( + state: &mut CheckState, + context: &CheckContext, + arguments: InferDoDiscard, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let InferDoDiscard { statement, discard_type, now_type, next_type, expression } = arguments; + state.with_error_step(ErrorStep::InferringDoDiscard(statement), move |state| { + let statement_type = + infer_do_discard_core(state, context, discard_type, next_type, expression)?; + let _ = unification::subtype(state, context, statement_type, now_type)?; + Ok(()) + }) +} + +fn infer_do_discard_core( state: &mut CheckState, context: &CheckContext, discard_type: TypeId, From da16d110da7cfd6845ad87bd64cb3d454f56bb10 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 06:08:50 +0800 Subject: [PATCH 027/386] Align infer_ado implementation to infer_do --- compiler-core/checking/src/algorithm/term.rs | 98 +++++++++++-------- compiler-core/checking/src/error.rs | 4 +- .../checking/218_ado_monad_error/Main.snap | 16 +-- 3 files changed, 69 insertions(+), 49 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 128f4ce0..8af6dcc4 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -764,17 +764,13 @@ where continue; }; if let Some(binder_type) = binder { - infer_do_bind( - state, - context, - InferDoBind { statement, bind_type, now_type, next_type, expression, binder_type }, - )?; + let arguments = + InferDoBind { statement, bind_type, now_type, next_type, expression, binder_type }; + infer_do_bind(state, context, arguments)?; } else { - infer_do_discard( - state, - context, - InferDoDiscard { statement, discard_type, now_type, next_type, expression }, - )?; + let arguments = + InferDoDiscard { statement, discard_type, now_type, next_type, expression }; + infer_do_discard(state, context, arguments)?; } } @@ -790,6 +786,12 @@ where Ok(first_suffix) } +struct AdoBindingStatement { + statement: lowering::DoStatementId, + binder_type: TypeId, + expression: lowering::ExpressionId, +} + #[tracing::instrument(skip_all, name = "infer_ado")] fn infer_ado( state: &mut CheckState, @@ -810,8 +812,7 @@ where // First, perform a forward pass where variable bindings are // bound to unification variables and let bindings are checked. // This is like inferring the lambda in a desugared representation. - let mut binder_types = vec![]; - let mut ado_statements = vec![]; + let mut ado_binding_statements = vec![]; for &statement_id in statement_ids.iter() { let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { continue; @@ -823,27 +824,33 @@ where } else { state.fresh_unification_type(context) }; - binder_types.push(binder_type); - ado_statements.push(*expression); + let Some(expression) = *expression else { continue }; + ado_binding_statements.push(AdoBindingStatement { + statement: statement_id, + binder_type, + expression, + }); } lowering::DoStatement::Let { statements } => { check_let_chunks(state, context, statements)?; } lowering::DoStatement::Discard { expression } => { let binder_type = state.fresh_unification_type(context); - binder_types.push(binder_type); - ado_statements.push(*expression); + let Some(expression) = *expression else { continue }; + ado_binding_statements.push(AdoBindingStatement { + statement: statement_id, + binder_type, + expression, + }); } } } - assert_eq!(binder_types.len(), ado_statements.len()); - // For ado blocks with no bindings, we apply pure to the expression. // // pure_type := a -> f a // expression := t - let [head_statement, tail_statements @ ..] = &ado_statements[..] else { + let [head_statement, tail_statements @ ..] = &ado_binding_statements[..] else { return if let Some(expression) = expression { check_function_term_application(state, context, pure_type, expression) } else { @@ -866,6 +873,9 @@ where // in_expression :: Effect Message // in_expression_type := ?in_expression // lambda_type := ?a -> ?b -> ?in_expression + let binder_types = + ado_binding_statements.iter().map(|statement| statement.binder_type).collect_vec(); + let in_expression_type = state.fresh_unification_type(context); let lambda_type = state.make_function(&binder_types, in_expression_type); @@ -888,8 +898,11 @@ where // >> ?a := String // // continuation_type := Effect (?b -> ?in_expression) - let mut continuation_type = - infer_ado_map(state, context, map_type, lambda_type, *head_statement)?; + let mut continuation_type = { + let AdoBindingStatement { statement, expression, .. } = *head_statement; + let arguments = InferAdoMap { statement, map_type, lambda_type, expression }; + infer_ado_map(state, context, arguments)? + }; // Then, the infer_ado_apply rule applies `apply` to the inferred // expression type and the continuation type that is a function @@ -904,9 +917,10 @@ where // >> ?b := Int // // continuation_type := Effect ?in_expression - for expression in tail_statements { - continuation_type = - infer_ado_apply(state, context, apply_type, continuation_type, *expression)?; + for tail_statement in tail_statements { + let AdoBindingStatement { statement, expression, .. } = *tail_statement; + let arguments = InferAdoApply { statement, apply_type, continuation_type, expression }; + continuation_type = infer_ado_apply(state, context, arguments)?; } // Finally, check the in-expression against in_expression. @@ -1128,22 +1142,25 @@ where } } +struct InferAdoMap { + statement: lowering::DoStatementId, + map_type: TypeId, + lambda_type: TypeId, + expression: lowering::ExpressionId, +} + #[tracing::instrument(skip_all, name = "infer_ado_map")] fn infer_ado_map( state: &mut CheckState, context: &CheckContext, - map_type: TypeId, - continuation_type: TypeId, - expression: Option, + arguments: InferAdoMap, ) -> QueryResult where Q: ExternalQueries, { - let Some(expression) = expression else { - return Ok(context.prim.unknown); - }; - state.with_error_step(ErrorStep::InferringAdoMap(expression), |state| { - infer_ado_map_core(state, context, map_type, continuation_type, expression) + let InferAdoMap { statement, map_type, lambda_type, expression } = arguments; + state.with_error_step(ErrorStep::InferringAdoMap(statement), |state| { + infer_ado_map_core(state, context, map_type, lambda_type, expression) }) } @@ -1188,21 +1205,24 @@ where ) } +struct InferAdoApply { + statement: lowering::DoStatementId, + apply_type: TypeId, + continuation_type: TypeId, + expression: lowering::ExpressionId, +} + #[tracing::instrument(skip_all, name = "infer_ado_apply")] fn infer_ado_apply( state: &mut CheckState, context: &CheckContext, - apply_type: TypeId, - continuation_type: TypeId, - expression: Option, + arguments: InferAdoApply, ) -> QueryResult where Q: ExternalQueries, { - let Some(expression) = expression else { - return Ok(context.prim.unknown); - }; - state.with_error_step(ErrorStep::InferringAdoApply(expression), |state| { + let InferAdoApply { statement, apply_type, continuation_type, expression } = arguments; + state.with_error_step(ErrorStep::InferringAdoApply(statement), |state| { infer_ado_apply_core(state, context, apply_type, continuation_type, expression) }) } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 4fecf2d8..90470ecd 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -26,8 +26,8 @@ pub enum ErrorStep { InferringDoBind(lowering::DoStatementId), InferringDoDiscard(lowering::DoStatementId), - InferringAdoMap(lowering::ExpressionId), - InferringAdoApply(lowering::ExpressionId), + InferringAdoMap(lowering::DoStatementId), + InferringAdoApply(lowering::DoStatementId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap index 931563fe..b17a8e28 100644 --- a/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/218_ado_monad_error/Main.snap @@ -25,22 +25,22 @@ Aff = [Nominal] Diagnostics error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 15:8..15:22 + --> 15:3..15:22 | 15 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?15[:0]' - --> 15:8..15:22 + --> 15:3..15:22 | 15 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' - --> 21:8..21:22 + --> 21:3..21:22 | 21 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?30[:0]' - --> 21:8..21:22 + --> 21:3..21:22 | 21 | b <- affPure "life" - | ^~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ From e1d49ff3d9513d265e862d7f6a4c1e409bf91c9f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 06:31:34 +0800 Subject: [PATCH 028/386] Capture premature unification behaviour from let bindings --- .../220_do_let_premature_solve/Main.purs | 28 +++++++++++++ .../220_do_let_premature_solve/Main.snap | 40 +++++++++++++++++++ .../221_do_let_annotation_solve/Main.purs | 20 ++++++++++ .../221_do_let_annotation_solve/Main.snap | 29 ++++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 5 files changed, 121 insertions(+) create mode 100644 tests-integration/fixtures/checking/220_do_let_premature_solve/Main.purs create mode 100644 tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap create mode 100644 tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.purs create mode 100644 tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap diff --git a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.purs b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.purs new file mode 100644 index 00000000..2aa41d84 --- /dev/null +++ b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.purs @@ -0,0 +1,28 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a. Effect a -> Effect Unit -> Effect Unit + +class Semiring a where + add :: a -> a -> a + +instance Semiring Int where + add = addImpl + +foreign import addImpl :: Int -> Int -> Int + +infixl 6 add as + + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = do + a <- thing1 + let f = a + 123 + pure unit diff --git a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap new file mode 100644 index 00000000..5c2f78e3 --- /dev/null +++ b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: forall (a :: Type). Effect (a :: Type) -> Effect Unit -> Effect Unit +add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +addImpl :: Int -> Int -> Int ++ :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type +Semiring :: Type -> Constraint + +Roles +Effect = [Nominal] +Unit = [] + +Classes +class Semiring (&0 :: Type) + +Instances +instance Semiring (Int :: Type) + chain: 0 + +Diagnostics +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 28:3..28:14 + | +28 | a <- thing1 + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.purs b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.purs new file mode 100644 index 00000000..9ab3a657 --- /dev/null +++ b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a. Effect a -> Effect Unit -> Effect Unit + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = do + a <- thing1 + let + f :: Int + f = a + pure unit diff --git a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap new file mode 100644 index 00000000..98e0d511 --- /dev/null +++ b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: forall (a :: Type). Effect (a :: Type) -> Effect Unit -> Effect Unit +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] + +Diagnostics +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 18:3..18:14 + | +18 | a <- thing1 + | ^~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 3d8ca0d0..8f36c76b 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -465,3 +465,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_218_ado_monad_error_main() { run_test("218_ado_monad_error", "Main"); } #[rustfmt::skip] #[test] fn test_219_do_mixed_monad_error_main() { run_test("219_do_mixed_monad_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_220_do_let_premature_solve_main() { run_test("220_do_let_premature_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_221_do_let_annotation_solve_main() { run_test("221_do_let_annotation_solve", "Main"); } From 94225b144fc60d1ab74958c5d997612e9dfb3dcc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 07:03:46 +0800 Subject: [PATCH 029/386] Check let statements in sequence in do expressions --- compiler-core/checking/src/algorithm/term.rs | 159 +++++++++++------- compiler-core/checking/src/error.rs | 1 + compiler-core/checking/src/trace.rs | 6 +- compiler-core/diagnostics/src/context.rs | 6 +- .../220_do_let_premature_solve/Main.snap | 13 +- .../221_do_let_annotation_solve/Main.snap | 6 +- 6 files changed, 113 insertions(+), 78 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 8af6dcc4..e3214f9d 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -622,10 +622,11 @@ where let bind_type = lookup_term_variable(state, context, bind)?; let discard_type = lookup_term_variable(state, context, discard)?; - // First, perform a forward pass where variable bindings are - // bound to unification variables and let bindings are checked. - // This is like inferring the lambda in a desugared representation. - let mut do_statements = vec![]; + // First, perform a forward pass where variable bindings are bound + // to unification variables. Let bindings are not checked here to + // avoid premature solving of unification variables. Instead, they + // are checked inline during the statement checking loop. + let mut steps = vec![]; for &statement_id in statement_ids.iter() { let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { continue; @@ -637,66 +638,46 @@ where } else { state.fresh_unification_type(context) }; - do_statements.push((statement_id, Some(binder_type), *expression)); + steps.push(DoStep::Bind { + statement: statement_id, + binder_type, + expression: *expression, + }); } lowering::DoStatement::Let { statements } => { - check_let_chunks(state, context, statements)?; + steps.push(DoStep::Let { statement: statement_id, statements: &statements }); } lowering::DoStatement::Discard { expression } => { - do_statements.push((statement_id, None, *expression)); + steps.push(DoStep::Discard { statement: statement_id, expression: *expression }); } } } - let [binding_statements @ .., (_, _, pure_expression)] = &do_statements[..] else { - state.insert_error(ErrorKind::EmptyDoBlock); - return Ok(context.prim.unknown); - }; + let action_count = steps + .iter() + .filter(|step| matches!(step, DoStep::Bind { .. } | DoStep::Discard { .. })) + .count(); - let Some(pure_expression) = pure_expression else { + let pure_expression = steps.iter().rev().find_map(|step| match step { + DoStep::Bind { expression, .. } | DoStep::Discard { expression, .. } => Some(*expression), + DoStep::Let { .. } => None, + }); + + let Some(Some(pure_expression)) = pure_expression else { + state.insert_error(ErrorKind::EmptyDoBlock); return Ok(context.prim.unknown); }; // Create unification variables that each statement in the do expression // will unify against. The next section will get into more detail how // these are used. These unification variables are used to enable GHC-like - // left-to-right checking of ado-expression while maintaining the same + // left-to-right checking of do expressions while maintaining the same // semantics as rebindable `do` in PureScript. - let continuation_count = do_statements.len(); let continuation_types = iter::repeat_with(|| state.fresh_unification_type(context)) - .take(continuation_count) + .take(action_count) .collect_vec(); - // Process statements left-to-right. Each statement: - // 1. Infers the expression type - // 2. Applies bind/discard with the next suffix type as continuation - // 3. Unifies the result with the current suffix type - // - // Example with Effect and Aff mixed: - // do - // a <- effect -- Effect Int - // b <- aff -- Aff Int (conflict!) - // pure { a, b } - // - // Statement 0 (a <- effect): - // bind :: m a -> (a -> m b) -> m b - // Instantiate: Effect Int -> (Int -> Effect ?b0) -> Effect ?b0 - // Continuation uses S[1], so S[1] ~ Effect ?b0 - // Result S[0] ~ Effect ?b0 - // - // Statement 1 (b <- aff): - // bind :: m a -> (a -> m b) -> m b - // Instantiate: Aff Int -> (Int -> Aff ?b1) -> Aff ?b1 - // Continuation uses S[2], result must match S[1] - // But S[1] = Effect ?b0, and result = Aff ?b1 - // CONFLICT at statement 1 (aff line) - correct attribution! - // - - let statements = binding_statements.iter().copied(); - let continuations = continuation_types.iter().tuple_windows::<(_, _)>(); - - // Iterate over each of the statements and a tuple window over the - // continuation types. Let's trace over the following example: + // Let's trace over the following example: // // main = do // a <- effect @@ -758,32 +739,80 @@ where // This unification error is expected, but this left-to-right checking // approach significantly improves the reported error positions compared // to the previous approach that emulated desugared checking. - for ((statement, binder, expression), (&now_type, &next_type)) in statements.zip(continuations) - { - let Some(expression) = expression else { - continue; - }; - if let Some(binder_type) = binder { - let arguments = - InferDoBind { statement, bind_type, now_type, next_type, expression, binder_type }; - infer_do_bind(state, context, arguments)?; - } else { - let arguments = - InferDoDiscard { statement, discard_type, now_type, next_type, expression }; - infer_do_discard(state, context, arguments)?; + + let mut continuations = continuation_types.iter().tuple_windows::<(_, _)>(); + + for step in &steps { + match step { + DoStep::Let { statement, statements } => { + let error_step = ErrorStep::CheckingDoLet(*statement); + state.with_error_step(error_step, |state| { + check_let_chunks(state, context, statements) + })?; + } + DoStep::Bind { statement, binder_type, expression } => { + let Some((&now_type, &next_type)) = continuations.next() else { + continue; + }; + let Some(expression) = *expression else { + continue; + }; + let arguments = InferDoBind { + statement: *statement, + bind_type, + now_type, + next_type, + expression, + binder_type: *binder_type, + }; + infer_do_bind(state, context, arguments)?; + } + DoStep::Discard { statement, expression } => { + let Some((&now_type, &next_type)) = continuations.next() else { + continue; + }; + let Some(expression) = *expression else { + continue; + }; + let arguments = InferDoDiscard { + statement: *statement, + discard_type, + now_type, + next_type, + expression, + }; + infer_do_discard(state, context, arguments)?; + } } } - // The `first_suffix` is the overall type of the do expression, built - // iteratively and through solving unification variables. Meanwhile, - // the `final_suffix` is the expected type for the final expression. - let [first_suffix, .., final_suffix] = continuation_types[..] else { - unreachable!("invariant violated: insufficient suffix_types"); + // The `first_continuation` is the overall type of the do expression, + // built iteratively and through solving unification variables. On + // the other hand, the `final_continuation` is the expected type for + // the final statement in the do expression. + let [first_continuation, .., final_continuation] = continuation_types[..] else { + unreachable!("invariant violated: insufficient continuation_types"); }; - check_expression(state, context, *pure_expression, final_suffix)?; + check_expression(state, context, pure_expression, final_continuation)?; + + Ok(first_continuation) +} - Ok(first_suffix) +enum DoStep<'a> { + Bind { + statement: lowering::DoStatementId, + binder_type: TypeId, + expression: Option, + }, + Discard { + statement: lowering::DoStatementId, + expression: Option, + }, + Let { + statement: lowering::DoStatementId, + statements: &'a [lowering::LetBindingChunk], + }, } struct AdoBindingStatement { diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 90470ecd..abbc704d 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -25,6 +25,7 @@ pub enum ErrorStep { InferringDoBind(lowering::DoStatementId), InferringDoDiscard(lowering::DoStatementId), + CheckingDoLet(lowering::DoStatementId), InferringAdoMap(lowering::DoStatementId), InferringAdoApply(lowering::DoStatementId), diff --git a/compiler-core/checking/src/trace.rs b/compiler-core/checking/src/trace.rs index 19ddf1b9..174495e9 100644 --- a/compiler-core/checking/src/trace.rs +++ b/compiler-core/checking/src/trace.rs @@ -32,9 +32,9 @@ where ErrorStep::TypeDeclaration(id) => { context.indexed.type_item_ptr(&context.stabilized, *id).next()? } - ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { - context.stabilized.syntax_ptr(*id)? - } + ErrorStep::InferringDoBind(id) + | ErrorStep::InferringDoDiscard(id) + | ErrorStep::CheckingDoLet(id) => context.stabilized.syntax_ptr(*id)?, ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { context.stabilized.syntax_ptr(*id)? } diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index c47a5fd1..d64a9ccc 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -118,9 +118,9 @@ impl<'a> DiagnosticsContext<'a> { ErrorStep::TypeDeclaration(id) => { self.indexed.type_item_ptr(self.stabilized, *id).next()? } - ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) => { - self.stabilized.syntax_ptr(*id)? - } + ErrorStep::InferringDoBind(id) + | ErrorStep::InferringDoDiscard(id) + | ErrorStep::CheckingDoLet(id) => self.stabilized.syntax_ptr(*id)?, ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { self.stabilized.syntax_ptr(*id)? } diff --git a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap index 5c2f78e3..5f8cf754 100644 --- a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap +++ b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap @@ -33,8 +33,13 @@ instance Semiring (Int :: Type) chain: 0 Diagnostics -error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 28:3..28:14 +error[CannotUnify]: Cannot unify 'Int' with 'String' + --> 27:15..27:18 | -28 | a <- thing1 - | ^~~~~~~~~~~ +27 | let f = a + 123 + | ^~~ +error[NoInstanceFound]: No instance found for: Semiring String + --> 24:1..24:20 + | +24 | test :: Effect Unit + | ^~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap index 98e0d511..c8374cb6 100644 --- a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap @@ -23,7 +23,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 18:3..18:14 + --> 17:3..19:10 | -18 | a <- thing1 - | ^~~~~~~~~~~ +17 | let + | ^~~ From ca2d1c459fb1aed764ab4e495c4316fd76017079 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 07:22:18 +0800 Subject: [PATCH 030/386] Capture premature unification behaviour in ado expressions --- .../222_ado_let_premature_solve/Main.purs | 28 +++++++++++ .../222_ado_let_premature_solve/Main.snap | 47 +++++++++++++++++++ .../223_ado_let_annotation_solve/Main.purs | 20 ++++++++ .../223_ado_let_annotation_solve/Main.snap | 36 ++++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 5 files changed, 135 insertions(+) create mode 100644 tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.purs create mode 100644 tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap create mode 100644 tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.purs create mode 100644 tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap diff --git a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.purs b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.purs new file mode 100644 index 00000000..77f9722c --- /dev/null +++ b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.purs @@ -0,0 +1,28 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +class Semiring a where + add :: a -> a -> a + +instance Semiring Int where + add = addImpl + +foreign import addImpl :: Int -> Int -> Int + +infixl 6 add as + + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = ado + a <- thing1 + let f = a + 123 + in unit diff --git a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap new file mode 100644 index 00000000..413071ab --- /dev/null +++ b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap @@ -0,0 +1,47 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +addImpl :: Int -> Int -> Int ++ :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type +Semiring :: Type -> Constraint + +Roles +Effect = [Nominal] +Unit = [] + +Classes +class Semiring (&0 :: Type) + +Instances +instance Semiring (Int :: Type) + chain: 0 + +Diagnostics +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 26:3..26:14 + | +26 | a <- thing1 + | ^~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Effect String' with 'Effect Int' + --> 26:3..26:14 + | +26 | a <- thing1 + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.purs b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.purs new file mode 100644 index 00000000..0728bc90 --- /dev/null +++ b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = ado + a <- thing1 + let + f :: Int + f = a + in unit diff --git a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap new file mode 100644 index 00000000..1d7c0780 --- /dev/null +++ b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap @@ -0,0 +1,36 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] + +Diagnostics +error[CannotUnify]: Cannot unify 'String' with 'Int' + --> 16:3..16:14 + | +16 | a <- thing1 + | ^~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Effect String' with 'Effect Int' + --> 16:3..16:14 + | +16 | a <- thing1 + | ^~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 8f36c76b..d0806518 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -469,3 +469,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_220_do_let_premature_solve_main() { run_test("220_do_let_premature_solve", "Main"); } #[rustfmt::skip] #[test] fn test_221_do_let_annotation_solve_main() { run_test("221_do_let_annotation_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_222_ado_let_premature_solve_main() { run_test("222_ado_let_premature_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_223_ado_let_annotation_solve_main() { run_test("223_ado_let_annotation_solve", "Main"); } From b6876fb1f7a55e430885da3fef52feacfa30da39 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 07:23:36 +0800 Subject: [PATCH 031/386] Check let statements in sequence in ado expressions --- compiler-core/checking/src/algorithm/term.rs | 138 +++++++++++------- compiler-core/checking/src/error.rs | 1 + compiler-core/checking/src/trace.rs | 6 +- compiler-core/diagnostics/src/context.rs | 6 +- .../222_ado_let_premature_solve/Main.snap | 16 +- .../223_ado_let_annotation_solve/Main.snap | 11 +- 6 files changed, 105 insertions(+), 73 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index e3214f9d..f40f24b0 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -645,7 +645,7 @@ where }); } lowering::DoStatement::Let { statements } => { - steps.push(DoStep::Let { statement: statement_id, statements: &statements }); + steps.push(DoStep::Let { statement: statement_id, statements: statements }); } lowering::DoStatement::Discard { expression } => { steps.push(DoStep::Discard { statement: statement_id, expression: *expression }); @@ -815,10 +815,16 @@ enum DoStep<'a> { }, } -struct AdoBindingStatement { - statement: lowering::DoStatementId, - binder_type: TypeId, - expression: lowering::ExpressionId, +enum AdoStep<'a> { + Action { + statement: lowering::DoStatementId, + binder_type: TypeId, + expression: lowering::ExpressionId, + }, + Let { + statement: lowering::DoStatementId, + statements: &'a [lowering::LetBindingChunk], + }, } #[tracing::instrument(skip_all, name = "infer_ado")] @@ -838,10 +844,11 @@ where let apply_type = lookup_term_variable(state, context, apply)?; let pure_type = lookup_term_variable(state, context, pure)?; - // First, perform a forward pass where variable bindings are - // bound to unification variables and let bindings are checked. - // This is like inferring the lambda in a desugared representation. - let mut ado_binding_statements = vec![]; + // First, perform a forward pass where variable bindings are bound + // to unification variables. Let bindings are not checked here to + // avoid premature solving of unification variables. Instead, they + // are checked inline during the statement checking loop. + let mut steps = vec![]; for &statement_id in statement_ids.iter() { let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { continue; @@ -854,39 +861,46 @@ where state.fresh_unification_type(context) }; let Some(expression) = *expression else { continue }; - ado_binding_statements.push(AdoBindingStatement { - statement: statement_id, - binder_type, - expression, - }); + steps.push(AdoStep::Action { statement: statement_id, binder_type, expression }); } lowering::DoStatement::Let { statements } => { - check_let_chunks(state, context, statements)?; + steps.push(AdoStep::Let { statement: statement_id, statements: statements }); } lowering::DoStatement::Discard { expression } => { let binder_type = state.fresh_unification_type(context); let Some(expression) = *expression else { continue }; - ado_binding_statements.push(AdoBindingStatement { - statement: statement_id, - binder_type, - expression, - }); + steps.push(AdoStep::Action { statement: statement_id, binder_type, expression }); } } } - // For ado blocks with no bindings, we apply pure to the expression. + let binder_types = steps.iter().filter_map(|step| match step { + AdoStep::Action { binder_type, .. } => Some(*binder_type), + AdoStep::Let { .. } => None, + }); + + let binder_types = binder_types.collect_vec(); + + // For ado blocks with no bindings, we check let statements and then + // apply pure to the expression. // // pure_type := a -> f a // expression := t - let [head_statement, tail_statements @ ..] = &ado_binding_statements[..] else { + if binder_types.is_empty() { + for step in &steps { + if let AdoStep::Let { statement, statements } = step { + state.with_error_step(ErrorStep::CheckingAdoLet(*statement), |state| { + check_let_chunks(state, context, statements) + })?; + } + } return if let Some(expression) = expression { check_function_term_application(state, context, pure_type, expression) } else { state.insert_error(ErrorKind::EmptyAdoBlock); Ok(context.prim.unknown) }; - }; + } // Create a fresh unification variable for the in_expression. // Inferring expression directly may solve the unification variables @@ -902,9 +916,6 @@ where // in_expression :: Effect Message // in_expression_type := ?in_expression // lambda_type := ?a -> ?b -> ?in_expression - let binder_types = - ado_binding_statements.iter().map(|statement| statement.binder_type).collect_vec(); - let in_expression_type = state.fresh_unification_type(context); let lambda_type = state.make_function(&binder_types, in_expression_type); @@ -914,9 +925,9 @@ where // // (\a _ -> Message a) <$> (pure "Hello!") <*> (pure 42) // - // To emulate this, the infer_ado_map rule applies `map` to the - // inferred expression type and the lambda type we synthesised - // using the unification variables we created previously. + // To emulate this, we process steps in source order. Let bindings + // are checked inline between map/apply operations. The first action + // uses infer_ado_map, and subsequent actions use infer_ado_apply. // // map_type :: (a -> b) -> f a -> f b // lambda_type := ?a -> ?b -> ?in_expression @@ -927,29 +938,50 @@ where // >> ?a := String // // continuation_type := Effect (?b -> ?in_expression) - let mut continuation_type = { - let AdoBindingStatement { statement, expression, .. } = *head_statement; - let arguments = InferAdoMap { statement, map_type, lambda_type, expression }; - infer_ado_map(state, context, arguments)? - }; + let mut continuation_type = None; - // Then, the infer_ado_apply rule applies `apply` to the inferred - // expression type and the continuation type that is a function - // contained within some container, like Effect. - // - // apply_type := f (x -> y) -> f x -> f y - // continuation_type := Effect (?b -> ?in_expression) - // - // expression_type := Effect Int - // apply(continuation, expression) := Effect ?in_expression - // >> - // >> ?b := Int - // - // continuation_type := Effect ?in_expression - for tail_statement in tail_statements { - let AdoBindingStatement { statement, expression, .. } = *tail_statement; - let arguments = InferAdoApply { statement, apply_type, continuation_type, expression }; - continuation_type = infer_ado_apply(state, context, arguments)?; + for step in &steps { + match step { + AdoStep::Let { statement, statements } => { + let error_step = ErrorStep::CheckingAdoLet(*statement); + state.with_error_step(error_step, |state| { + check_let_chunks(state, context, statements) + })?; + } + AdoStep::Action { statement, expression, .. } => { + let statement_type = if let Some(continuation_type) = continuation_type { + // Then, the infer_ado_apply rule applies `apply` to the inferred + // expression type and the continuation type that is a function + // contained within some container, like Effect. + // + // apply_type := f (x -> y) -> f x -> f y + // continuation_type := Effect (?b -> ?in_expression) + // + // expression_type := Effect Int + // apply(continuation, expression) := Effect ?in_expression + // >> + // >> ?b := Int + // + // continuation_type := Effect ?in_expression + let arguments = InferAdoApply { + statement: *statement, + apply_type, + continuation_type, + expression: *expression, + }; + infer_ado_apply(state, context, arguments)? + } else { + let arguments = InferAdoMap { + statement: *statement, + map_type, + lambda_type, + expression: *expression, + }; + infer_ado_map(state, context, arguments)? + }; + continuation_type = Some(statement_type); + } + } } // Finally, check the in-expression against in_expression. @@ -964,6 +996,10 @@ where check_expression(state, context, expression, in_expression_type)?; } + let Some(continuation_type) = continuation_type else { + unreachable!("invariant violated: impossible empty steps"); + }; + Ok(continuation_type) } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index abbc704d..5dba8475 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -29,6 +29,7 @@ pub enum ErrorStep { InferringAdoMap(lowering::DoStatementId), InferringAdoApply(lowering::DoStatementId), + CheckingAdoLet(lowering::DoStatementId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/compiler-core/checking/src/trace.rs b/compiler-core/checking/src/trace.rs index 174495e9..551ff4e4 100644 --- a/compiler-core/checking/src/trace.rs +++ b/compiler-core/checking/src/trace.rs @@ -35,9 +35,9 @@ where ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) | ErrorStep::CheckingDoLet(id) => context.stabilized.syntax_ptr(*id)?, - ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { - context.stabilized.syntax_ptr(*id)? - } + ErrorStep::InferringAdoMap(id) + | ErrorStep::InferringAdoApply(id) + | ErrorStep::CheckingAdoLet(id) => context.stabilized.syntax_ptr(*id)?, }; let range = pointer.text_range(); diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index d64a9ccc..26d13738 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -121,9 +121,9 @@ impl<'a> DiagnosticsContext<'a> { ErrorStep::InferringDoBind(id) | ErrorStep::InferringDoDiscard(id) | ErrorStep::CheckingDoLet(id) => self.stabilized.syntax_ptr(*id)?, - ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) => { - self.stabilized.syntax_ptr(*id)? - } + ErrorStep::InferringAdoMap(id) + | ErrorStep::InferringAdoApply(id) + | ErrorStep::CheckingAdoLet(id) => self.stabilized.syntax_ptr(*id)?, }; self.span_from_syntax_ptr(&ptr) } diff --git a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap index 413071ab..7f3d205d 100644 --- a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap +++ b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap @@ -35,13 +35,13 @@ instance Semiring (Int :: Type) chain: 0 Diagnostics -error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 26:3..26:14 +error[CannotUnify]: Cannot unify 'Int' with 'String' + --> 27:15..27:18 | -26 | a <- thing1 - | ^~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Effect String' with 'Effect Int' - --> 26:3..26:14 +27 | let f = a + 123 + | ^~~ +error[NoInstanceFound]: No instance found for: Semiring String + --> 24:1..24:20 | -26 | a <- thing1 - | ^~~~~~~~~~~ +24 | test :: Effect Unit + | ^~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap index 1d7c0780..35fefc28 100644 --- a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap @@ -25,12 +25,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 16:3..16:14 + --> 17:3..19:10 | -16 | a <- thing1 - | ^~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Effect String' with 'Effect Int' - --> 16:3..16:14 - | -16 | a <- thing1 - | ^~~~~~~~~~~ +17 | let + | ^~~ From e5f7288e0721437cf511db99d9049e1df756358e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 08:41:47 +0800 Subject: [PATCH 032/386] Incomplete thought on do expression checking --- compiler-core/checking/src/algorithm/term.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index f40f24b0..87ea0998 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -713,6 +713,8 @@ where // // For the second statement, we know the following information. // The `now` type was already solved by the previous statement, + // and an error should surface once we check the inferred type + // of the statement against it. // // expression_type := Aff Int // binder_type := ?b From ffeffddf4403cb34bebce715fb6ccff84ceef504 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 09:19:32 +0800 Subject: [PATCH 033/386] Identify bug in checking record patterns smaller than expected --- .../checking/224_record_shrinking/Main.purs | 4 ++++ .../checking/224_record_shrinking/Main.snap | 21 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 tests-integration/fixtures/checking/224_record_shrinking/Main.purs create mode 100644 tests-integration/fixtures/checking/224_record_shrinking/Main.snap diff --git a/tests-integration/fixtures/checking/224_record_shrinking/Main.purs b/tests-integration/fixtures/checking/224_record_shrinking/Main.purs new file mode 100644 index 00000000..9a86121a --- /dev/null +++ b/tests-integration/fixtures/checking/224_record_shrinking/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } -> Int +test { a } = a diff --git a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap new file mode 100644 index 00000000..366595db --- /dev/null +++ b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } -> Int + +Types + +Diagnostics +error[CannotUnify]: Cannot unify '( a :: Int )' with '( a :: Int, b :: Int )' + --> 4:6..4:11 + | +4 | test { a } = a + | ^~~~~ +error[CannotUnify]: Cannot unify '{ a :: Int }' with '{ a :: Int, b :: Int }' + --> 4:6..4:11 + | +4 | test { a } = a + | ^~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index d0806518..215f10e9 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -473,3 +473,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_222_ado_let_premature_solve_main() { run_test("222_ado_let_premature_solve", "Main"); } #[rustfmt::skip] #[test] fn test_223_ado_let_annotation_solve_main() { run_test("223_ado_let_annotation_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_224_record_shrinking_main() { run_test("224_record_shrinking", "Main"); } From 2c8ac611103ae0adc0f54283d28486543458471d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 21:20:54 +0800 Subject: [PATCH 034/386] Infer open rows for record binders --- .../checking/src/algorithm/binder.rs | 12 +- compiler-core/checking/src/algorithm/kind.rs | 5 +- compiler-core/checking/src/algorithm/state.rs | 6 +- compiler-core/checking/src/algorithm/term.rs | 8 +- .../checking/src/algorithm/unification.rs | 174 ++++++++++++++---- compiler-core/checking/src/error.rs | 8 +- compiler-core/diagnostics/src/convert.rs | 16 ++ .../checking/061_record_binder/Main.snap | 17 +- .../checking/224_record_shrinking/Main.snap | 12 -- .../Main.purs | 4 + .../Main.snap | 16 ++ .../Main.purs | 4 + .../Main.snap | 16 ++ tests-integration/tests/checking/generated.rs | 4 + 14 files changed, 227 insertions(+), 75 deletions(-) create mode 100644 tests-integration/fixtures/checking/225_record_binder_additional_property/Main.purs create mode 100644 tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap create mode 100644 tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.purs create mode 100644 tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index abc8bdc2..02750841 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -217,7 +217,6 @@ where let label = SmolStr::clone(name); let id = infer_binder(state, context, *value)?; - fields.push(RowField { label, id }); } lowering::BinderRecordItem::RecordPun { id, name } => { @@ -225,14 +224,16 @@ where let label = SmolStr::clone(name); let field_type = state.fresh_unification_type(context); - state.term_scope.bind_pun(*id, field_type); + state.term_scope.bind_pun(*id, field_type); fields.push(RowField { label, id: field_type }); } } } - let row_type = RowType::from_unsorted(fields, None); + let row_tail = state.fresh_unification_kinded(context.prim.row_type); + + let row_type = RowType::from_unsorted(fields, Some(row_tail)); let row_type = state.storage.intern(Type::Row(row_type)); let record_type = @@ -240,9 +241,10 @@ where if let BinderMode::Check { expected_type } = mode { unification::subtype(state, context, record_type, expected_type)?; + Ok(expected_type) + } else { + Ok(record_type) } - - Ok(record_type) } lowering::BinderKind::Parenthesized { parenthesized } => { diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index 044f81b2..de8ec2b2 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -234,11 +234,8 @@ where } lowering::TypeKind::Record { items, tail } => { - let expected_kind = - state.storage.intern(Type::Application(context.prim.row, context.prim.t)); - let (row_t, row_k) = infer_row_kind(state, context, items, tail)?; - let _ = unification::subtype(state, context, row_k, expected_kind)?; + let _ = unification::subtype(state, context, row_k, context.prim.row_type)?; let t = state.storage.intern(Type::Application(context.prim.record, row_t)); let k = context.prim.t; diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 31090e3b..4f68a275 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -467,6 +467,7 @@ pub struct PrimCore { pub constraint: TypeId, pub symbol: TypeId, pub row: TypeId, + pub row_type: TypeId, pub unknown: TypeId, } @@ -477,6 +478,8 @@ impl PrimCore { let t = lookup.type_constructor("Type"); let type_to_type = lookup.intern(Type::Function(t, t)); + let row = lookup.type_constructor("Row"); + let row_type = lookup.intern(Type::Application(row, t)); Ok(PrimCore { t, @@ -492,7 +495,8 @@ impl PrimCore { partial: lookup.type_constructor("Partial"), constraint: lookup.type_constructor("Constraint"), symbol: lookup.type_constructor("Symbol"), - row: lookup.type_constructor("Row"), + row, + row_type, unknown: lookup.intern(Type::Unknown), }) } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 87ea0998..1ddd2c65 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1090,10 +1090,7 @@ where let field_type = state.fresh_unification_type(context); - let row_type_kind = - state.storage.intern(Type::Application(context.prim.row, context.prim.t)); - - let tail_type = state.fresh_unification_kinded(row_type_kind); + let tail_type = state.fresh_unification_kinded(context.prim.row_type); let row_type = RowType::from_unsorted(vec![RowField { label, id: field_type }], Some(tail_type)); @@ -1179,8 +1176,7 @@ where } } - let row_type_kind = state.storage.intern(Type::Application(context.prim.row, context.prim.t)); - let tail = state.fresh_unification_kinded(row_type_kind); + let tail = state.fresh_unification_kinded(context.prim.row_type); Ok((input_fields, output_fields, tail)) } diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index df312438..dbc23941 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -92,6 +92,23 @@ where subtype(state, context, inner, t2) } + ( + Type::Application(t1_function, t1_argument), + Type::Application(t2_function, t2_argument), + ) if t1_function == context.prim.record && t2_function == context.prim.record => { + let t1_argument = synonym::normalize_expand_type(state, context, t1_argument)?; + let t2_argument = synonym::normalize_expand_type(state, context, t2_argument)?; + + let t1_core = state.storage[t1_argument].clone(); + let t2_core = state.storage[t2_argument].clone(); + + if let (Type::Row(t1_row), Type::Row(t2_row)) = (t1_core, t2_core) { + subtype_record_rows(state, context, &t1_row, &t2_row) + } else { + unify(state, context, t1, t2) + } + } + (_, _) => unify(state, context, t1, t2), } } @@ -333,64 +350,81 @@ pub fn promote_type( } } -fn unify_rows( +/// Checks that `t1_row` is a subtype of `t2_row`, generated errors for +/// additional or missing fields. This is used for record subtyping. +/// +/// * This algorithm partitions row fields into common, t1-only, and t2-only fields. +/// * If t1_row is closed and t2_row is non-empty, [`ErrorKind::PropertyIsMissing`] +/// * If t2_row is closed and t1_row is non-empty, [`ErrorKind::AdditionalProperty`] +#[tracing::instrument(skip_all, name = "subtype_rows")] +fn subtype_record_rows( state: &mut CheckState, context: &CheckContext, - t1_row: RowType, - t2_row: RowType, + t1_row: &RowType, + t2_row: &RowType, ) -> QueryResult where Q: ExternalQueries, { - let (extras_left, extras_right, ok) = partition_row_fields(state, context, &t1_row, &t2_row)?; + let (left_only, right_only, ok) = partition_row_fields_with( + state, + context, + t1_row, + t2_row, + |state, context, left, right| { + Ok(subtype(state, context, left, right)? && subtype(state, context, right, left)?) + }, + )?; if !ok { return Ok(false); } - match (t1_row.tail, t2_row.tail) { - (None, None) => Ok(extras_left.is_empty() && extras_right.is_empty()), - - (Some(t1_tail), None) => { - if !extras_left.is_empty() { - return Ok(false); - } - let row = Type::Row(RowType { fields: Arc::from(extras_right), tail: None }); - let row_id = state.storage.intern(row); - unify(state, context, t1_tail, row_id) - } + if t1_row.tail.is_none() && !right_only.is_empty() { + let labels = right_only.iter().map(|field| field.label.clone()); + let labels = Arc::from_iter(labels); + state.insert_error(ErrorKind::PropertyIsMissing { labels }); + return Ok(false); + } - (None, Some(t2_tail)) => { - if !extras_right.is_empty() { - return Ok(false); - } - let row = Type::Row(RowType { fields: Arc::from(extras_left), tail: None }); - let row_id = state.storage.intern(row); - unify(state, context, t2_tail, row_id) - } + if t2_row.tail.is_none() && !left_only.is_empty() { + let labels = left_only.iter().map(|field| field.label.clone()); + let labels = Arc::from_iter(labels); + state.insert_error(ErrorKind::AdditionalProperty { labels }); + return Ok(false); + } - (Some(t1_tail), Some(t2_tail)) => { - if extras_left.is_empty() && extras_right.is_empty() { - return unify(state, context, t1_tail, t2_tail); - } + unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) +} - let row_type_kind = - state.storage.intern(Type::Application(context.prim.row, context.prim.t)); +fn unify_rows( + state: &mut CheckState, + context: &CheckContext, + t1_row: RowType, + t2_row: RowType, +) -> QueryResult +where + Q: ExternalQueries, +{ + let (left_only, right_only, ok) = partition_row_fields(state, context, &t1_row, &t2_row)?; - let unification = state.fresh_unification_kinded(row_type_kind); + if !ok { + return Ok(false); + } - let left_tail_row = - Type::Row(RowType { fields: Arc::from(extras_right), tail: Some(unification) }); - let left_tail_row_id = state.storage.intern(left_tail_row); + if t1_row.tail.is_none() && t2_row.tail.is_none() { + return Ok(left_only.is_empty() && right_only.is_empty()); + } - let right_tail_row = - Type::Row(RowType { fields: Arc::from(extras_left), tail: Some(unification) }); - let right_tail_row_id = state.storage.intern(right_tail_row); + if t2_row.tail.is_none() && !left_only.is_empty() { + return Ok(false); + } - Ok(unify(state, context, t1_tail, left_tail_row_id)? - && unify(state, context, t2_tail, right_tail_row_id)?) - } + if t1_row.tail.is_none() && !right_only.is_empty() { + return Ok(false); } + + unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) } pub fn partition_row_fields( @@ -401,6 +435,20 @@ pub fn partition_row_fields( ) -> QueryResult<(Vec, Vec, bool)> where Q: ExternalQueries, +{ + partition_row_fields_with(state, context, t1_row, t2_row, unify) +} + +fn partition_row_fields_with( + state: &mut CheckState, + context: &CheckContext, + t1_row: &RowType, + t2_row: &RowType, + mut field_check: F, +) -> QueryResult<(Vec, Vec, bool)> +where + Q: ExternalQueries, + F: FnMut(&mut CheckState, &CheckContext, TypeId, TypeId) -> QueryResult, { let mut extras_left = vec![]; let mut extras_right = vec![]; @@ -412,7 +460,7 @@ where for field in t1_fields.merge_join_by(t2_fields, |left, right| left.label.cmp(&right.label)) { match field { EitherOrBoth::Both(left, right) => { - if !unify(state, context, left.id, right.id)? { + if !field_check(state, context, left.id, right.id)? { ok = false; } } @@ -429,3 +477,49 @@ where Ok((extras_left, extras_right, ok)) } + +fn unify_row_tails( + state: &mut CheckState, + context: &CheckContext, + t1_tail: Option, + t2_tail: Option, + extras_left: Vec, + extras_right: Vec, +) -> QueryResult +where + Q: ExternalQueries, +{ + match (t1_tail, t2_tail) { + (None, None) => Ok(true), + + (Some(t1_tail), None) => { + let row = Type::Row(RowType { fields: Arc::from(extras_right), tail: None }); + let row_id = state.storage.intern(row); + unify(state, context, t1_tail, row_id) + } + + (None, Some(t2_tail)) => { + let row = Type::Row(RowType { fields: Arc::from(extras_left), tail: None }); + let row_id = state.storage.intern(row); + unify(state, context, t2_tail, row_id) + } + + (Some(t1_tail), Some(t2_tail)) => { + if extras_left.is_empty() && extras_right.is_empty() { + return unify(state, context, t1_tail, t2_tail); + } + + let unification = state.fresh_unification_kinded(context.prim.row_type); + let tail = Some(unification); + + let left_tail_row = Type::Row(RowType { fields: Arc::from(extras_right), tail }); + let left_tail_row_id = state.storage.intern(left_tail_row); + + let right_tail_row = Type::Row(RowType { fields: Arc::from(extras_left), tail }); + let right_tail_row_id = state.storage.intern(right_tail_row); + + Ok(unify(state, context, t1_tail, left_tail_row_id)? + && unify(state, context, t2_tail, right_tail_row_id)?) + } + } +} diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 5dba8475..d1a72223 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -32,7 +32,7 @@ pub enum ErrorStep { CheckingAdoLet(lowering::DoStatementId), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ErrorKind { AmbiguousConstraint { constraint: TypeErrorMessageId, @@ -114,6 +114,12 @@ pub enum ErrorKind { CustomFailure { message_id: TypeErrorMessageId, }, + PropertyIsMissing { + labels: Arc<[SmolStr]>, + }, + AdditionalProperty { + labels: Arc<[SmolStr]>, + }, } #[derive(Debug, PartialEq, Eq)] diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 3bf0ed1f..7a651d09 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -286,6 +286,22 @@ impl ToDiagnostics for CheckError { let msg = lookup_message(*message_id); (Severity::Error, "CustomFailure", msg.to_string()) } + ErrorKind::PropertyIsMissing { labels } => { + let labels_str = labels.join(", "); + ( + Severity::Error, + "PropertyIsMissing", + format!("Missing required properties: {labels_str}"), + ) + } + ErrorKind::AdditionalProperty { labels } => { + let labels_str = labels.join(", "); + ( + Severity::Error, + "AdditionalProperty", + format!("Additional properties not allowed: {labels_str}"), + ) + } }; vec![Diagnostic { diff --git a/tests-integration/fixtures/checking/061_record_binder/Main.snap b/tests-integration/fixtures/checking/061_record_binder/Main.snap index b5887cdb..1c4d6738 100644 --- a/tests-integration/fixtures/checking/061_record_binder/Main.snap +++ b/tests-integration/fixtures/checking/061_record_binder/Main.snap @@ -1,20 +1,25 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms test1 :: { x :: Int, y :: Int } -> Int test1' :: - forall (t10 :: Type) (t11 :: Type). { x :: (t11 :: Type), y :: (t10 :: Type) } -> (t11 :: Type) + forall (t12 :: Type) (t13 :: Row Type) (t14 :: Type). + { x :: (t14 :: Type), y :: (t12 :: Type) | (t13 :: Row Type) } -> (t14 :: Type) test2 :: { x :: Int, y :: String } -> { x :: Int, y :: String } test2' :: - forall (t15 :: Type) (t16 :: Type). - { x :: (t15 :: Type), y :: (t16 :: Type) } -> { x :: (t15 :: Type), y :: (t16 :: Type) } + forall (t20 :: Type) (t21 :: Type) (t22 :: Row Type). + { x :: (t20 :: Type), y :: (t21 :: Type) | (t22 :: Row Type) } -> + { x :: (t20 :: Type), y :: (t21 :: Type) } test3 :: { age :: Int, name :: String } -> String test3' :: - forall (t22 :: Type) (t23 :: Type). - { age :: (t22 :: Type), name :: (t23 :: Type) } -> (t23 :: Type) + forall (t30 :: Type) (t31 :: Row Type) (t32 :: Type). + { age :: (t30 :: Type), name :: (t32 :: Type) | (t31 :: Row Type) } -> (t32 :: Type) nested :: { inner :: { x :: Int } } -> Int -nested' :: forall (t27 :: Type). { inner :: { x :: (t27 :: Type) } } -> (t27 :: Type) +nested' :: + forall (t40 :: Row Type) (t41 :: Row Type) (t42 :: Type). + { inner :: { x :: (t42 :: Type) | (t40 :: Row Type) } | (t41 :: Row Type) } -> (t42 :: Type) Types diff --git a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap index 366595db..b79528bb 100644 --- a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap +++ b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap @@ -7,15 +7,3 @@ Terms test :: { a :: Int, b :: Int } -> Int Types - -Diagnostics -error[CannotUnify]: Cannot unify '( a :: Int )' with '( a :: Int, b :: Int )' - --> 4:6..4:11 - | -4 | test { a } = a - | ^~~~~ -error[CannotUnify]: Cannot unify '{ a :: Int }' with '{ a :: Int, b :: Int }' - --> 4:6..4:11 - | -4 | test { a } = a - | ^~~~~ diff --git a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.purs b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.purs new file mode 100644 index 00000000..bc79aeef --- /dev/null +++ b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int } -> Int +test { a, b, c } = a diff --git a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap new file mode 100644 index 00000000..95be51e9 --- /dev/null +++ b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int } -> Int + +Types + +Diagnostics +error[AdditionalProperty]: Additional properties not allowed: b, c + --> 4:6..4:17 + | +4 | test { a, b, c } = a + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.purs b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.purs new file mode 100644 index 00000000..f14006b4 --- /dev/null +++ b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { outer :: { x :: Int } } -> Int +test { outer: { x, y } } = x diff --git a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap new file mode 100644 index 00000000..8465399f --- /dev/null +++ b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { outer :: { x :: Int } } -> Int + +Types + +Diagnostics +error[AdditionalProperty]: Additional properties not allowed: y + --> 4:6..4:25 + | +4 | test { outer: { x, y } } = x + | ^~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 215f10e9..036e378b 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -475,3 +475,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_223_ado_let_annotation_solve_main() { run_test("223_ado_let_annotation_solve", "Main"); } #[rustfmt::skip] #[test] fn test_224_record_shrinking_main() { run_test("224_record_shrinking", "Main"); } + +#[rustfmt::skip] #[test] fn test_225_record_binder_additional_property_main() { run_test("225_record_binder_additional_property", "Main"); } + +#[rustfmt::skip] #[test] fn test_226_record_binder_additional_property_nested_main() { run_test("226_record_binder_additional_property_nested", "Main"); } From 3d9c476c047181ad6683839d5b51332325d4d3b3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 21:28:05 +0800 Subject: [PATCH 035/386] Add basic tests for closed record expressions --- .../227_record_expression_exact_match/Main.purs | 4 ++++ .../227_record_expression_exact_match/Main.snap | 9 +++++++++ .../Main.purs | 4 ++++ .../Main.snap | 16 ++++++++++++++++ .../Main.purs | 4 ++++ .../Main.snap | 16 ++++++++++++++++ .../Main.purs | 4 ++++ .../Main.snap | 16 ++++++++++++++++ .../Main.purs | 4 ++++ .../Main.snap | 16 ++++++++++++++++ tests-integration/tests/checking/generated.rs | 10 ++++++++++ 11 files changed, 103 insertions(+) create mode 100644 tests-integration/fixtures/checking/227_record_expression_exact_match/Main.purs create mode 100644 tests-integration/fixtures/checking/227_record_expression_exact_match/Main.snap create mode 100644 tests-integration/fixtures/checking/228_record_expression_missing_field/Main.purs create mode 100644 tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap create mode 100644 tests-integration/fixtures/checking/229_record_expression_additional_field/Main.purs create mode 100644 tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap create mode 100644 tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.purs create mode 100644 tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap create mode 100644 tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.purs create mode 100644 tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap diff --git a/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.purs b/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.purs new file mode 100644 index 00000000..80032018 --- /dev/null +++ b/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1, b: 2 } diff --git a/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.snap b/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.snap new file mode 100644 index 00000000..de1ecb3c --- /dev/null +++ b/tests-integration/fixtures/checking/227_record_expression_exact_match/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types diff --git a/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.purs b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.purs new file mode 100644 index 00000000..b38d9667 --- /dev/null +++ b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1 } diff --git a/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap new file mode 100644 index 00000000..6e33b6dc --- /dev/null +++ b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types + +Diagnostics +error[PropertyIsMissing]: Missing required properties: b + --> 3:1..3:31 + | +3 | test :: { a :: Int, b :: Int } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.purs b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.purs new file mode 100644 index 00000000..2deff69d --- /dev/null +++ b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int } +test = { a: 1, b: 2 } diff --git a/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap new file mode 100644 index 00000000..7c939f10 --- /dev/null +++ b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int } + +Types + +Diagnostics +error[AdditionalProperty]: Additional properties not allowed: b + --> 3:1..3:21 + | +3 | test :: { a :: Int } + | ^~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.purs b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.purs new file mode 100644 index 00000000..bdd8fb6d --- /dev/null +++ b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1, c: 3 } diff --git a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap new file mode 100644 index 00000000..6e33b6dc --- /dev/null +++ b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types + +Diagnostics +error[PropertyIsMissing]: Missing required properties: b + --> 3:1..3:31 + | +3 | test :: { a :: Int, b :: Int } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.purs b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.purs new file mode 100644 index 00000000..e11b1035 --- /dev/null +++ b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { outer :: { x :: Int } } +test = { outer: { x: 1, y: 2 } } diff --git a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap new file mode 100644 index 00000000..398d4f08 --- /dev/null +++ b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { outer :: { x :: Int } } + +Types + +Diagnostics +error[AdditionalProperty]: Additional properties not allowed: y + --> 3:1..3:34 + | +3 | test :: { outer :: { x :: Int } } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 036e378b..a9cc3ca4 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -479,3 +479,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_225_record_binder_additional_property_main() { run_test("225_record_binder_additional_property", "Main"); } #[rustfmt::skip] #[test] fn test_226_record_binder_additional_property_nested_main() { run_test("226_record_binder_additional_property_nested", "Main"); } + +#[rustfmt::skip] #[test] fn test_227_record_expression_exact_match_main() { run_test("227_record_expression_exact_match", "Main"); } + +#[rustfmt::skip] #[test] fn test_228_record_expression_missing_field_main() { run_test("228_record_expression_missing_field", "Main"); } + +#[rustfmt::skip] #[test] fn test_229_record_expression_additional_field_main() { run_test("229_record_expression_additional_field", "Main"); } + +#[rustfmt::skip] #[test] fn test_230_record_expression_missing_and_additional_main() { run_test("230_record_expression_missing_and_additional", "Main"); } + +#[rustfmt::skip] #[test] fn test_231_record_expression_nested_additional_main() { run_test("231_record_expression_nested_additional", "Main"); } From 1027bdfb888becd2dc2df9488233a15d653001e2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 24 Jan 2026 21:30:53 +0800 Subject: [PATCH 036/386] Report both PropertyIsMissing and AdditionalProperty --- compiler-core/checking/src/algorithm/unification.rs | 8 +++++++- .../Main.snap | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index dbc23941..ec1be867 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -380,17 +380,23 @@ where return Ok(false); } + let mut failed = false; + if t1_row.tail.is_none() && !right_only.is_empty() { let labels = right_only.iter().map(|field| field.label.clone()); let labels = Arc::from_iter(labels); state.insert_error(ErrorKind::PropertyIsMissing { labels }); - return Ok(false); + failed = true; } if t2_row.tail.is_none() && !left_only.is_empty() { let labels = left_only.iter().map(|field| field.label.clone()); let labels = Arc::from_iter(labels); state.insert_error(ErrorKind::AdditionalProperty { labels }); + failed = true; + } + + if failed { return Ok(false); } diff --git a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap index 6e33b6dc..50a1ab16 100644 --- a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap +++ b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap @@ -14,3 +14,8 @@ error[PropertyIsMissing]: Missing required properties: b | 3 | test :: { a :: Int, b :: Int } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[AdditionalProperty]: Additional properties not allowed: c + --> 3:1..3:31 + | +3 | test :: { a :: Int, b :: Int } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 4efbdc0a76911a4ac4bffe3972af904ad988a5d9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 25 Jan 2026 02:01:33 +0800 Subject: [PATCH 037/386] Add kind application instantiation for check_surface_kind --- compiler-core/checking/src/algorithm/kind.rs | 64 +++++++++++++++++-- .../checking/src/algorithm/kind/synonym.rs | 1 + .../032_recursive_synonym_expansion/Main.snap | 45 +++++-------- .../checking/081_prim_rowlist/Main.snap | 3 +- .../Main.snap | 5 +- .../Main.purs | 13 ++++ .../Main.snap | 20 ++++++ tests-integration/tests/checking/generated.rs | 2 + ...g__recursive_synonym_expansion_errors.snap | 43 +++++++------ 9 files changed, 139 insertions(+), 57 deletions(-) create mode 100644 tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.purs create mode 100644 tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index de8ec2b2..2af33955 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -10,6 +10,7 @@ use lowering::TypeVariableBindingId; use smol_str::SmolStr; use crate::ExternalQueries; +use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{substitute, transfer, unification}; use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, Variable}; @@ -522,26 +523,81 @@ where Ok(type_id) } +/// Instantiates kind-level foralls using [`Type::KindApplication`]. +/// +/// If the inferred kind is polymorphic, and the inferred kind is monomorphic, +/// this function adds the necessary kind applications to the inferred type. +/// For example, when checking `RowList.Nil` against `RowList Type`: +/// +/// ```text +/// Nil :: forall k. RowList k. +/// +/// check(Nil, RowList Type) +/// infer(Nil) -> forall k. RowList k +/// instantiate(Nil) -> (Nil @?t, RowList ?t) +/// +/// subtype(RowList ?t, RowList Type) +/// solve(?t, Type) +/// +/// t := Nil @Type +/// k := RowList Type +/// ``` +fn instantiate_kind_applications( + state: &mut CheckState, + mut t: TypeId, + mut k: TypeId, + expected_kind: TypeId, +) -> (TypeId, TypeId) { + let expected_kind = state.normalize_type(expected_kind); + + if matches!(state.storage[expected_kind], Type::Forall(_, _)) { + return (t, k); + } + + safe_loop! { + k = state.normalize_type(k); + + let Type::Forall(ref binder, inner_kind) = state.storage[k] else { + break; + }; + + let binder_level = binder.level; + let binder_kind = state.normalize_type(binder.kind); + + let argument_type = state.fresh_unification_kinded(binder_kind); + t = state.storage.intern(Type::KindApplication(t, argument_type)); + k = substitute::SubstituteBound::on(state, binder_level, argument_type, inner_kind); + } + + (t, k) +} + #[tracing::instrument(skip_all, name = "check_surface_kind")] pub fn check_surface_kind( state: &mut CheckState, context: &CheckContext, id: lowering::TypeId, - kind: TypeId, + expected_kind: TypeId, ) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { - crate::trace_fields!(state, context, { expected_kind = kind }); + crate::trace_fields!(state, context, { expected_kind = expected_kind }); state.with_error_step(ErrorStep::CheckingKind(id), |state| { let (inferred_type, inferred_kind) = infer_surface_kind_core(state, context, id)?; - let _ = unification::subtype(state, context, inferred_kind, kind)?; + + let (inferred_type, inferred_kind) = + instantiate_kind_applications(state, inferred_type, inferred_kind, expected_kind); + + let _ = unification::subtype(state, context, inferred_kind, expected_kind)?; + crate::trace_fields!(state, context, { inferred_type = inferred_type, inferred_kind = inferred_kind, - expected_kind = kind + expected_kind = expected_kind }); + Ok((inferred_type, inferred_kind)) }) } diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 9de92bbb..562f19d6 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -131,6 +131,7 @@ pub fn infer_synonym_constructor( } if is_recursive_synonym(context, file_id, type_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id: type_id }); let synonym_type = state.storage.intern(Type::Constructor(file_id, type_id)); return Ok((synonym_type, kind)); } diff --git a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap index 1e5cca2f..7ac4b8b4 100644 --- a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap @@ -4,9 +4,9 @@ assertion_line: 28 expression: report --- Terms -testF :: F -> F -testG :: G -> G -testH :: H -> H +testF :: F @Type -> F @Type +testG :: G @Type -> G @Type +testH :: H @Type -> H @Type testValid :: Int -> Int Types @@ -39,47 +39,32 @@ Valid = Int Diagnostics error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 8:1..8:16 + --> 8:10..8:11 | 8 | testF :: F -> F - | ^~~~~~~~~~~~~~~ + | ^ error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 8:1..8:16 + --> 8:15..8:16 | 8 | testF :: F -> F - | ^~~~~~~~~~~~~~~ + | ^ error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 8:1..8:16 - | -8 | testF :: F -> F - | ^~~~~~~~~~~~~~~ -error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 11:1..11:16 + --> 11:10..11:11 | 11 | testG :: G -> G - | ^~~~~~~~~~~~~~~ + | ^ error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 11:1..11:16 + --> 11:15..11:16 | 11 | testG :: G -> G - | ^~~~~~~~~~~~~~~ -error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 11:1..11:16 - | -11 | testG :: G -> G - | ^~~~~~~~~~~~~~~ -error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 14:1..14:16 - | -14 | testH :: H -> H - | ^~~~~~~~~~~~~~~ + | ^ error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 14:1..14:16 + --> 14:10..14:11 | 14 | testH :: H -> H - | ^~~~~~~~~~~~~~~ + | ^ error[RecursiveSynonymExpansion]: Recursive type synonym expansion - --> 14:1..14:16 + --> 14:15..14:16 | 14 | testH :: H -> H - | ^~~~~~~~~~~~~~~ + | ^ diff --git a/tests-integration/fixtures/checking/081_prim_rowlist/Main.snap b/tests-integration/fixtures/checking/081_prim_rowlist/Main.snap index 14ef4e47..d0366721 100644 --- a/tests-integration/fixtures/checking/081_prim_rowlist/Main.snap +++ b/tests-integration/fixtures/checking/081_prim_rowlist/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -8,7 +9,7 @@ Types Test :: RowList Type Synonyms -Test = Cons @Type "T" Int Nil +Test = Cons @Type "T" Int (Nil @Type) Quantified = :0 Kind = :0 Type = :0 diff --git a/tests-integration/fixtures/checking/199_coercible_higher_kinded_polykinded/Main.snap b/tests-integration/fixtures/checking/199_coercible_higher_kinded_polykinded/Main.snap index 7f300c09..3adc95cf 100644 --- a/tests-integration/fixtures/checking/199_coercible_higher_kinded_polykinded/Main.snap +++ b/tests-integration/fixtures/checking/199_coercible_higher_kinded_polykinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,8 +14,8 @@ MaybeAlias :: forall (k :: Type) (n :: (k :: Type)) (a :: Type). Maybe @(k :: Type) (n :: (k :: Type)) (a :: Type) -> MaybeAlias @(k :: Type) (n :: (k :: Type)) (a :: Type) -coerceContainer :: Container Maybe -> Container MaybeAlias -coerceContainerReverse :: Container MaybeAlias -> Container Maybe +coerceContainer :: Container (Maybe @Type) -> Container (MaybeAlias @Type) +coerceContainerReverse :: Container (MaybeAlias @Type) -> Container (Maybe @Type) Types Maybe :: forall (k :: Type). (k :: Type) -> Type -> Type diff --git a/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.purs b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.purs new file mode 100644 index 00000000..30a2be50 --- /dev/null +++ b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.purs @@ -0,0 +1,13 @@ +module Main where + +import Prim.RowList as RL + +class ListToRow :: RL.RowList Type -> Constraint +class ListToRow xs + +instance listToRowNil :: ListToRow RL.Nil + +class ListToRow2 :: RL.RowList Type -> RL.RowList Type -> Constraint +class ListToRow2 xs ys + +instance listToRow2Nil :: ListToRow2 RL.Nil RL.Nil diff --git a/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap new file mode 100644 index 00000000..b7f558ec --- /dev/null +++ b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +ListToRow :: RowList Type -> Constraint +ListToRow2 :: RowList Type -> RowList Type -> Constraint + +Classes +class ListToRow (&0 :: RowList Type) +class ListToRow2 (&0 :: RowList Type) (&1 :: RowList Type) + +Instances +instance ListToRow (Nil @Type :: RowList Type) + chain: 0 +instance ListToRow2 (Nil @Type :: RowList Type) (Nil @Type :: RowList Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a9cc3ca4..3ba2d82a 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -489,3 +489,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_230_record_expression_missing_and_additional_main() { run_test("230_record_expression_missing_and_additional", "Main"); } #[rustfmt::skip] #[test] fn test_231_record_expression_nested_additional_main() { run_test("231_record_expression_nested_additional", "Main"); } + +#[rustfmt::skip] #[test] fn test_232_instance_head_nil_kind_application_main() { run_test("232_instance_head_nil_kind_application", "Main"); } diff --git a/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap b/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap index 725a637c..3319db7d 100644 --- a/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap +++ b/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking.rs +assertion_line: 974 expression: checked.errors --- [ @@ -12,16 +13,11 @@ expression: checked.errors TermDeclaration( Idx::(0), ), - ], - }, - CheckError { - kind: RecursiveSynonymExpansion { - file_id: Idx::(9), - item_id: Idx::(0), - }, - step: [ - TermDeclaration( - Idx::(0), + CheckingKind( + AstId(12), + ), + CheckingKind( + AstId(13), ), ], }, @@ -34,16 +30,11 @@ expression: checked.errors TermDeclaration( Idx::(0), ), - ], - }, - CheckError { - kind: RecursiveSynonymExpansion { - file_id: Idx::(9), - item_id: Idx::(2), - }, - step: [ - TermDeclaration( - Idx::(1), + CheckingKind( + AstId(12), + ), + CheckingKind( + AstId(14), ), ], }, @@ -56,6 +47,12 @@ expression: checked.errors TermDeclaration( Idx::(1), ), + CheckingKind( + AstId(22), + ), + CheckingKind( + AstId(23), + ), ], }, CheckError { @@ -67,6 +64,12 @@ expression: checked.errors TermDeclaration( Idx::(1), ), + CheckingKind( + AstId(22), + ), + CheckingKind( + AstId(24), + ), ], }, ] From 8888192d54fe7503f10a3f2f3f281cf31d06780d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 26 Jan 2026 05:10:39 +0800 Subject: [PATCH 038/386] Add get_all_determined function --- .../constraint/functional_dependency.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/compiler-core/checking/src/algorithm/constraint/functional_dependency.rs b/compiler-core/checking/src/algorithm/constraint/functional_dependency.rs index ed0ecd4c..92423b9b 100644 --- a/compiler-core/checking/src/algorithm/constraint/functional_dependency.rs +++ b/compiler-core/checking/src/algorithm/constraint/functional_dependency.rs @@ -19,6 +19,11 @@ impl Fd { } } +/// Computes the set of positions that are determined by any functional dependency. +pub fn get_all_determined(functional_dependencies: &[Fd]) -> HashSet { + functional_dependencies.iter().flat_map(|fd| fd.determined.iter().copied()).collect() +} + /// Compute the closure of positions determined by functional dependencies. /// /// Starting from `initial` positions, iteratively applies fundeps: @@ -123,4 +128,42 @@ mod tests { let result = compute_closure(&fundeps, &initial); assert_eq!(result, [0].into_iter().collect()); } + + #[test] + fn test_all_determined_no_fundeps() { + let result = get_all_determined(&[]); + assert_eq!(result, HashSet::new()); + } + + #[test] + fn test_all_determined_single_fundep() { + // a -> b (position 0 determines position 1) + let fundeps = vec![Fd::new([0], [1])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1].into_iter().collect()); + } + + #[test] + fn test_all_determined_multiple_fundeps() { + // a -> b, b -> c + let fundeps = vec![Fd::new([0], [1]), Fd::new([1], [2])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1, 2].into_iter().collect()); + } + + #[test] + fn test_all_determined_overlapping() { + // a -> b c, d -> b + let fundeps = vec![Fd::new([0], [1, 2]), Fd::new([3], [1])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1, 2].into_iter().collect()); + } + + #[test] + fn test_all_determined_empty_determiners() { + // -> a (empty determiners, determines position 0) + let fundeps = vec![Fd::new([], [0])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [0].into_iter().collect()); + } } From 9dbac15597b0a2d132c5bf8fef35eed3f198f182 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 26 Jan 2026 05:11:33 +0800 Subject: [PATCH 039/386] Add traversal for checking labeled rows --- compiler-core/checking/src/algorithm/visit.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/compiler-core/checking/src/algorithm/visit.rs b/compiler-core/checking/src/algorithm/visit.rs index 86ebd0fc..f7cc80a6 100644 --- a/compiler-core/checking/src/algorithm/visit.rs +++ b/compiler-core/checking/src/algorithm/visit.rs @@ -39,6 +39,31 @@ impl TypeVisitor for CollectFileReferences<'_> { } } +/// Checks if a type contains any rows with labels. +pub struct HasLabeledRole { + contains: bool, +} + +impl HasLabeledRole { + pub fn on(state: &mut CheckState, id: TypeId) -> bool { + let mut visitor = HasLabeledRole { contains: false }; + visit_type(state, id, &mut visitor); + visitor.contains + } +} + +impl TypeVisitor for HasLabeledRole { + fn visit(&mut self, _state: &mut CheckState, _id: TypeId, t: &Type) -> VisitAction { + if let Type::Row(RowType { fields, .. }) = t + && !fields.is_empty() + { + self.contains = true; + return VisitAction::Stop; + } + VisitAction::Continue + } +} + /// Recursively visit a type without transforming it. pub fn visit_type(state: &mut CheckState, id: TypeId, visitor: &mut V) { let id = state.normalize_type(id); From 53f758f8db721cffe8b50ba8136922f4bd2e8214 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 26 Jan 2026 05:11:54 +0800 Subject: [PATCH 040/386] Add error variant for labeled rows in instance heads --- compiler-core/checking/src/error.rs | 6 ++++++ compiler-core/diagnostics/src/convert.rs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index d1a72223..6b909107 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -69,6 +69,12 @@ pub enum ErrorKind { expected: usize, actual: usize, }, + InstanceHeadLabeledRow { + class_file: files::FileId, + class_item: indexing::TypeItemId, + position: usize, + type_message: TypeErrorMessageId, + }, InstanceMemberTypeMismatch { expected: TypeErrorMessageId, actual: TypeErrorMessageId, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 7a651d09..7c2b9870 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -221,6 +221,18 @@ impl ToDiagnostics for CheckError { "InstanceHeadMismatch", format!("Instance head mismatch: expected {expected} arguments, got {actual}"), ), + ErrorKind::InstanceHeadLabeledRow { position, type_message, .. } => { + let type_msg = lookup_message(*type_message); + ( + Severity::Error, + "InstanceHeadLabeledRow", + format!( + "Instance argument at position {position} contains a labeled row, \ + but this position is not determined by any functional dependency. \ + Only the `( | r )` form is allowed. Got '{type_msg}' instead." + ), + ) + } ErrorKind::InstanceMemberTypeMismatch { expected, actual } => { let expected = lookup_message(*expected); let actual = lookup_message(*actual); From dcb4069e699122bed0b15387733a0dabb9cbfda6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 26 Jan 2026 05:12:02 +0800 Subject: [PATCH 041/386] Tiny clippy fix --- compiler-core/checking/src/algorithm/term.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 1ddd2c65..fec76537 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -645,7 +645,7 @@ where }); } lowering::DoStatement::Let { statements } => { - steps.push(DoStep::Let { statement: statement_id, statements: statements }); + steps.push(DoStep::Let { statement: statement_id, statements }); } lowering::DoStatement::Discard { expression } => { steps.push(DoStep::Discard { statement: statement_id, expression: *expression }); @@ -866,7 +866,7 @@ where steps.push(AdoStep::Action { statement: statement_id, binder_type, expression }); } lowering::DoStatement::Let { statements } => { - steps.push(AdoStep::Let { statement: statement_id, statements: statements }); + steps.push(AdoStep::Let { statement: statement_id, statements }); } lowering::DoStatement::Discard { expression } => { let binder_type = state.fresh_unification_type(context); From 745bd6ced5c08d430d302c3a68090df1d9bb95be Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 26 Jan 2026 06:23:03 +0800 Subject: [PATCH 042/386] Implement validation for rows in instance heads --- .../checking/src/algorithm/constraint.rs | 180 ++++++++++++++++-- .../checking/src/algorithm/derive/tools.rs | 8 + .../checking/src/algorithm/term_item.rs | 8 + .../233_record_instance_matching/Main.purs | 21 ++ .../233_record_instance_matching/Main.snap | 25 +++ .../234_record_instance_open_row/Main.purs | 27 +++ .../234_record_instance_open_row/Main.snap | 26 +++ .../235_instance_head_invalid_row/Main.purs | 12 ++ .../235_instance_head_invalid_row/Main.snap | 55 ++++++ tests-integration/tests/checking/generated.rs | 6 + 10 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 tests-integration/fixtures/checking/233_record_instance_matching/Main.purs create mode 100644 tests-integration/fixtures/checking/233_record_instance_matching/Main.snap create mode 100644 tests-integration/fixtures/checking/234_record_instance_open_row/Main.purs create mode 100644 tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap create mode 100644 tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.purs create mode 100644 tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 671fb025..f936a2a8 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -5,7 +5,7 @@ mod compiler_solved; mod functional_dependency; use compiler_solved::*; -use functional_dependency::Fd; +use functional_dependency::{Fd, get_all_determined}; use std::collections::{HashSet, VecDeque}; use std::iter; @@ -19,9 +19,11 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::algorithm::fold::{FoldAction, TypeFold, fold_type}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::visit::{CollectFileReferences, TypeVisitor, VisitAction, visit_type}; +use crate::algorithm::visit::{ + CollectFileReferences, HasLabeledRole, TypeVisitor, VisitAction, visit_type, +}; use crate::algorithm::{toolkit, transfer, unification}; -use crate::core::{Class, Instance, InstanceKind, Variable, debruijn}; +use crate::core::{self, Class, Instance, InstanceKind, Variable, debruijn}; use crate::{CheckedModule, ExternalQueries, Type, TypeId}; #[tracing::instrument(skip_all, name = "solve_constraints")] @@ -461,13 +463,17 @@ enum MatchInstance { /// We use the [`can_unify`] function to speculate if these two types can be /// unified, or if unifying them solves unification variables, encoded by the /// [`CanUnify::Unify`] variant. -fn match_type( +fn match_type( state: &mut CheckState, + context: &CheckContext, bindings: &mut FxHashMap, equalities: &mut Vec<(TypeId, TypeId)>, wanted: TypeId, given: TypeId, -) -> MatchType { +) -> MatchType +where + Q: ExternalQueries, +{ let wanted = state.normalize_type(wanted); let given = state.normalize_type(given); @@ -497,30 +503,36 @@ fn match_type( (Type::Unification(_), _) => MatchType::Stuck, + (Type::Row(wanted_row), Type::Row(given_row)) => { + let wanted_row = wanted_row.clone(); + let given_row = given_row.clone(); + match_row_type(state, context, bindings, equalities, wanted_row, given_row) + } + ( &Type::Application(w_function, w_argument), &Type::Application(g_function, g_argument), - ) => match_type(state, bindings, equalities, w_function, g_function) - .and_also(|| match_type(state, bindings, equalities, w_argument, g_argument)), + ) => match_type(state, context, bindings, equalities, w_function, g_function) + .and_also(|| match_type(state, context, bindings, equalities, w_argument, g_argument)), (&Type::Function(w_argument, w_result), &Type::Function(g_argument, g_result)) => { - match_type(state, bindings, equalities, w_argument, g_argument) - .and_also(|| match_type(state, bindings, equalities, w_result, g_result)) + match_type(state, context, bindings, equalities, w_argument, g_argument) + .and_also(|| match_type(state, context, bindings, equalities, w_result, g_result)) } ( &Type::KindApplication(w_function, w_argument), &Type::KindApplication(g_function, g_argument), - ) => match_type(state, bindings, equalities, w_function, g_function) - .and_also(|| match_type(state, bindings, equalities, w_argument, g_argument)), + ) => match_type(state, context, bindings, equalities, w_function, g_function) + .and_also(|| match_type(state, context, bindings, equalities, w_argument, g_argument)), ( &Type::OperatorApplication(f1, t1, l1, r1), &Type::OperatorApplication(f2, t2, l2, r2), ) => { if f1 == f2 && t1 == t2 { - match_type(state, bindings, equalities, l1, l2) - .and_also(|| match_type(state, bindings, equalities, r1, r2)) + match_type(state, context, bindings, equalities, l1, l2) + .and_also(|| match_type(state, context, bindings, equalities, r1, r2)) } else { MatchType::Apart } @@ -531,7 +543,7 @@ fn match_type( let a1 = Arc::clone(a1); let a2 = Arc::clone(a2); iter::zip(a1.iter(), a2.iter()).fold(MatchType::Match, |result, (&a1, &a2)| { - result.and_also(|| match_type(state, bindings, equalities, a1, a2)) + result.and_also(|| match_type(state, context, bindings, equalities, a1, a2)) }) } else { MatchType::Apart @@ -542,6 +554,105 @@ fn match_type( } } +/// Matches row types in instance heads. +/// +/// This function handles structural row matching for both the tail variable +/// form `( | r )` in determiner positions and labeled rows in determined +/// positions `( x :: T | r )`. This function partitions the two row types, +/// matches the shared fields, and handles the row tail. +fn match_row_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &mut FxHashMap, + equalities: &mut Vec<(TypeId, TypeId)>, + wanted_row: core::RowType, + given_row: core::RowType, +) -> MatchType +where + Q: ExternalQueries, +{ + let mut wanted_only = vec![]; + let mut given_only = vec![]; + let mut result = MatchType::Match; + + let wanted_fields = wanted_row.fields.iter(); + let given_fields = given_row.fields.iter(); + + for field in itertools::merge_join_by(wanted_fields, given_fields, |wanted, given| { + wanted.label.cmp(&given.label) + }) { + match field { + itertools::EitherOrBoth::Both(wanted, given) => { + result = result.and_also(|| { + match_type(state, context, bindings, equalities, wanted.id, given.id) + }); + if matches!(result, MatchType::Apart) { + return MatchType::Apart; + } + } + itertools::EitherOrBoth::Left(wanted) => wanted_only.push(wanted), + itertools::EitherOrBoth::Right(given) => given_only.push(given), + } + } + + enum RowRest { + /// `( a :: Int )` and `( a :: Int | r )` + Additional, + /// `( | r )` + Open(TypeId), + /// `( )` + Closed, + } + + impl RowRest { + fn new(only: &[&core::RowField], tail: Option) -> RowRest { + if !only.is_empty() { + RowRest::Additional + } else if let Some(tail) = tail { + RowRest::Open(tail) + } else { + RowRest::Closed + } + } + } + + let given_rest = RowRest::new(&given_only, given_row.tail); + let wanted_rest = RowRest::new(&wanted_only, wanted_row.tail); + + use RowRest::*; + + match given_rest { + // If there are additional given fields + Additional => match wanted_rest { + // we cannot match it against a tail-less wanted, + // nor against the additional wanted fields. + Closed | Additional => MatchType::Apart, + // we could potentially make progress by having the + // wanted tail absorb the additional given fields + Open(_) => MatchType::Stuck, + }, + + // If the given row has a tail, match it against the + // additional fields and tail from the wanted row + Open(given_tail) => { + let fields = Arc::from_iter(wanted_only.into_iter().cloned()); + let row = core::RowType { fields, tail: wanted_row.tail }; + let row_id = state.storage.intern(Type::Row(row)); + result.and_also(|| match_type(state, context, bindings, equalities, row_id, given_tail)) + } + + // If we have a closed given row + Closed => match wanted_rest { + // we cannot match it against fields in the wanted row + Additional => MatchType::Apart, + // we could make progress with an open wanted row + Open(_) => MatchType::Stuck, + // we can match it directly with a closed wanted row + Closed => result, + }, + } +} + /// Matches an argument from a wanted constraint to one from a given constraint. /// /// This function is specialised for matching given constraints, like those @@ -758,13 +869,13 @@ where let mut bindings = FxHashMap::default(); let mut equalities = vec![]; - let mut match_results = vec![]; let mut stuck_positions = vec![]; for (index, (wanted, (given, _))) in arguments.iter().zip(&instance.arguments).enumerate() { let given = transfer::localize(state, context, *given); - let match_result = match_type(state, &mut bindings, &mut equalities, *wanted, given); + let match_result = + match_type(state, context, &mut bindings, &mut equalities, *wanted, given); if matches!(match_result, MatchType::Apart) { crate::trace_fields!(state, context, { ?wanted = wanted, ?given = given }, "apart"); @@ -1030,3 +1141,40 @@ impl TypeVisitor for CollectBoundVariables<'_> { VisitAction::Continue } } + +/// Validates that all rows in instance declaration arguments +/// do not have labels in non-determined positions. +/// +/// In PureScript, instance declarations can only contain rows with labels +/// in positions that are determined by functional dependencies. In the +/// determiner position, only row variables such as `( | r )` are valid. +pub fn validate_instance_rows( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_item: TypeItemId, + arguments: &[(TypeId, TypeId)], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let functional_dependencies = get_functional_dependencies(context, class_file, class_item)?; + let all_determined = get_all_determined(&functional_dependencies); + + for (position, &(argument_type, _)) in arguments.iter().enumerate() { + if all_determined.contains(&position) { + continue; + } + if HasLabeledRole::on(state, argument_type) { + let type_message = state.render_local_type(context, argument_type); + state.insert_error(crate::error::ErrorKind::InstanceHeadLabeledRow { + class_file, + class_item, + position, + type_message, + }); + } + } + + Ok(()) +} diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 37910019..34626635 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -124,6 +124,14 @@ pub fn register_derived_instance( quantify::quantify_instance(state, &mut instance); + let _ = constraint::validate_instance_rows( + state, + context, + class_file, + class_id, + &instance.arguments, + ); + for (t, k) in instance.arguments.iter_mut() { *t = transfer::globalize(state, context, *t); *k = transfer::globalize(state, context, *k); diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 96711917..2ab4439b 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -168,6 +168,14 @@ where quantify::quantify_instance(state, &mut instance); + constraint::validate_instance_rows( + state, + context, + class_file, + class_item, + &instance.arguments, + )?; + let arguments = instance.arguments.iter().map(|&(t, k)| { let t = transfer::globalize(state, context, t); let k = transfer::globalize(state, context, k); diff --git a/tests-integration/fixtures/checking/233_record_instance_matching/Main.purs b/tests-integration/fixtures/checking/233_record_instance_matching/Main.purs new file mode 100644 index 00000000..1d49266a --- /dev/null +++ b/tests-integration/fixtures/checking/233_record_instance_matching/Main.purs @@ -0,0 +1,21 @@ +module Main where + +class Make :: Type -> Type -> Constraint +class Make a b | a -> b where + make :: a -> b + +instance Make { | r } { | r } where + make x = x + +testMake :: { a :: Int, b :: String } -> { a :: Int, b :: String } +testMake = make + +class Convert :: Type -> Type -> Constraint +class Convert a b | a -> b where + convert :: a -> b + +instance Convert { | r } { converted :: { | r } } where + convert x = { converted: x } + +testConvert :: { x :: Int } -> { converted :: { x :: Int } } +testConvert = convert diff --git a/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap new file mode 100644 index 00000000..f8bf6fd0 --- /dev/null +++ b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +make :: forall (a :: Type) (b :: Type). Make (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testMake :: { a :: Int, b :: String } -> { a :: Int, b :: String } +convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testConvert :: { x :: Int } -> { converted :: { x :: Int } } + +Types +Make :: Type -> Type -> Constraint +Convert :: Type -> Type -> Constraint + +Classes +class Make (&0 :: Type) (&1 :: Type) +class Convert (&0 :: Type) (&1 :: Type) + +Instances +instance Make ({ | (&0 :: Row Type) } :: Type) ({ | (&0 :: Row Type) } :: Type) + chain: 0 +instance Convert ({ | (&0 :: Row Type) } :: Type) ({ converted :: { | (&0 :: Row Type) } } :: Type) + chain: 0 diff --git a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.purs b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.purs new file mode 100644 index 00000000..314c9740 --- /dev/null +++ b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.purs @@ -0,0 +1,27 @@ +module Main where + +class Clone :: Type -> Type -> Constraint +class Clone a b | a -> b where + clone :: a -> b + +instance Clone { | r } { | r } where + clone x = x + +clonePerson :: { name :: String, age :: Int } -> { name :: String, age :: Int } +clonePerson = clone + +cloneEmpty :: {} -> {} +cloneEmpty = clone + +cloneSingle :: { x :: Int } -> { x :: Int } +cloneSingle = clone + +class Nest :: Type -> Type -> Constraint +class Nest a b | a -> b where + nest :: a -> b + +instance Nest { | r } { inner :: { | r }, outer :: Int } where + nest x = { inner: x, outer: 0 } + +testNest :: { a :: String } -> { inner :: { a :: String }, outer :: Int } +testNest = nest diff --git a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap new file mode 100644 index 00000000..4246fda0 --- /dev/null +++ b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +clone :: forall (a :: Type) (b :: Type). Clone (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +clonePerson :: { age :: Int, name :: String } -> { age :: Int, name :: String } +cloneEmpty :: {} -> {} +cloneSingle :: { x :: Int } -> { x :: Int } +nest :: forall (a :: Type) (b :: Type). Nest (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testNest :: { a :: String } -> { inner :: { a :: String }, outer :: Int } + +Types +Clone :: Type -> Type -> Constraint +Nest :: Type -> Type -> Constraint + +Classes +class Clone (&0 :: Type) (&1 :: Type) +class Nest (&0 :: Type) (&1 :: Type) + +Instances +instance Clone ({ | (&0 :: Row Type) } :: Type) ({ | (&0 :: Row Type) } :: Type) + chain: 0 +instance Nest ({ | (&0 :: Row Type) } :: Type) ({ inner :: { | (&0 :: Row Type) }, outer :: Int } :: Type) + chain: 0 diff --git a/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.purs b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.purs new file mode 100644 index 00000000..461d9b6f --- /dev/null +++ b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.purs @@ -0,0 +1,12 @@ +module Main where + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +class T :: forall k. k -> Type +class T a + +instance T ( a :: Int ) +instance T { a :: Int } +instance T (Proxy ( a :: Int )) +instance T (Proxy { a :: Int }) diff --git a/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap new file mode 100644 index 00000000..2c12f568 --- /dev/null +++ b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap @@ -0,0 +1,55 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type +T :: forall (k :: Type). (k :: Type) -> Type + +Data +Proxy + Quantified = :0 + Kind = :1 + + +Roles +Proxy = [Phantom] + +Classes +class T (&1 :: (&0 :: Type)) + +Instances +instance T (( a :: Int ) :: Row Type) + chain: 0 +instance T ({ a :: Int } :: Type) + chain: 0 +instance T (Proxy @(Row Type) ( a :: Int ) :: Type) + chain: 0 +instance T (Proxy @Type { a :: Int } :: Type) + chain: 0 + +Diagnostics +error[InstanceHeadLabeledRow]: Instance argument at position 0 contains a labeled row, but this position is not determined by any functional dependency. Only the `( | r )` form is allowed. Got '( a :: Int )' instead. + --> 9:1..9:24 + | +9 | instance T ( a :: Int ) + | ^~~~~~~~~~~~~~~~~~~~~~~ +error[InstanceHeadLabeledRow]: Instance argument at position 0 contains a labeled row, but this position is not determined by any functional dependency. Only the `( | r )` form is allowed. Got '{ a :: Int }' instead. + --> 10:1..10:24 + | +10 | instance T { a :: Int } + | ^~~~~~~~~~~~~~~~~~~~~~~ +error[InstanceHeadLabeledRow]: Instance argument at position 0 contains a labeled row, but this position is not determined by any functional dependency. Only the `( | r )` form is allowed. Got 'Proxy @(Row Type) ( a :: Int )' instead. + --> 11:1..11:32 + | +11 | instance T (Proxy ( a :: Int )) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[InstanceHeadLabeledRow]: Instance argument at position 0 contains a labeled row, but this position is not determined by any functional dependency. Only the `( | r )` form is allowed. Got 'Proxy @Type { a :: Int }' instead. + --> 12:1..12:32 + | +12 | instance T (Proxy { a :: Int }) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 3ba2d82a..c79a4c64 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -491,3 +491,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_231_record_expression_nested_additional_main() { run_test("231_record_expression_nested_additional", "Main"); } #[rustfmt::skip] #[test] fn test_232_instance_head_nil_kind_application_main() { run_test("232_instance_head_nil_kind_application", "Main"); } + +#[rustfmt::skip] #[test] fn test_233_record_instance_matching_main() { run_test("233_record_instance_matching", "Main"); } + +#[rustfmt::skip] #[test] fn test_234_record_instance_open_row_main() { run_test("234_record_instance_open_row", "Main"); } + +#[rustfmt::skip] #[test] fn test_235_instance_head_invalid_row_main() { run_test("235_instance_head_invalid_row", "Main"); } From 081ede17668b889a69e55380815b9f26174b3b0b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 27 Jan 2026 03:15:59 +0800 Subject: [PATCH 043/386] Add unification rule for application-based Function --- compiler-core/checking/src/algorithm/state.rs | 13 ++++++-- .../checking/src/algorithm/unification.rs | 33 +++++++++++++++++++ .../236_category_function_instance/Main.purs | 21 ++++++++++++ .../236_category_function_instance/Main.snap | 33 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/236_category_function_instance/Main.purs create mode 100644 tests-integration/fixtures/checking/236_category_function_instance/Main.snap diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 4f68a275..251370cc 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -453,9 +453,11 @@ impl<'r, 's> PrimLookup<'r, 's> { } pub struct PrimCore { + pub prim_id: FileId, pub t: TypeId, pub type_to_type: TypeId, pub function: TypeId, + pub function_item: TypeItemId, pub array: TypeId, pub record: TypeId, pub number: TypeId, @@ -473,18 +475,25 @@ pub struct PrimCore { impl PrimCore { fn collect(queries: &impl ExternalQueries, state: &mut CheckState) -> QueryResult { - let resolved = queries.resolved(queries.prim_id())?; + let prim_id = queries.prim_id(); + let resolved = queries.resolved(prim_id)?; let mut lookup = PrimLookup::new(&resolved, &mut state.storage, "Prim"); let t = lookup.type_constructor("Type"); let type_to_type = lookup.intern(Type::Function(t, t)); + let row = lookup.type_constructor("Row"); let row_type = lookup.intern(Type::Application(row, t)); + let function = lookup.type_constructor("Function"); + let function_item = lookup.type_item("Function"); + Ok(PrimCore { + prim_id, t, type_to_type, - function: lookup.type_constructor("Function"), + function, + function_item, array: lookup.type_constructor("Array"), record: lookup.type_constructor("Record"), number: lookup.type_constructor("Number"), diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index ec1be867..add20506 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -165,6 +165,39 @@ where && unify(state, context, t1_result, t2_result)? } + // Unify Application(Application(f, a), b) with Function(a', b'). + // + // This handles the case where `f` is a unification variable that should + // be solved to `Function`. For example, when checking: + // + // identity :: forall t. Category a => a t t + // + // monomorphic :: forall a. a -> a + // monomorphic = identity + // + // Unifying `?a ?t ?t` and `a -> a` solves `?a := Function`. + (Type::Application(t1_partial, t1_result), Type::Function(t2_argument, t2_result)) => { + let t1_partial = state.normalize_type(t1_partial); + if let Type::Application(t1_constructor, t1_argument) = state.storage[t1_partial] { + unify(state, context, t1_constructor, context.prim.function)? + && unify(state, context, t1_argument, t2_argument)? + && unify(state, context, t1_result, t2_result)? + } else { + false + } + } + + (Type::Function(t1_argument, t1_result), Type::Application(t2_partial, t2_result)) => { + let t2_partial = state.normalize_type(t2_partial); + if let Type::Application(t2_constructor, t2_argument) = state.storage[t2_partial] { + unify(state, context, t2_constructor, context.prim.function)? + && unify(state, context, t1_argument, t2_argument)? + && unify(state, context, t1_result, t2_result)? + } else { + false + } + } + (Type::Row(t1_row), Type::Row(t2_row)) => unify_rows(state, context, t1_row, t2_row)?, (Type::Unification(unification_id), _) => { diff --git a/tests-integration/fixtures/checking/236_category_function_instance/Main.purs b/tests-integration/fixtures/checking/236_category_function_instance/Main.purs new file mode 100644 index 00000000..98a83df5 --- /dev/null +++ b/tests-integration/fixtures/checking/236_category_function_instance/Main.purs @@ -0,0 +1,21 @@ +module Main where + +class Semigroupoid :: forall k. (k -> k -> Type) -> Constraint +class Semigroupoid a where + compose :: forall b c d. a c d -> a b c -> a b d + +class Category :: forall k. (k -> k -> Type) -> Constraint +class Semigroupoid a <= Category a where + identity :: forall t. a t t + +instance semigroupoidFn :: Semigroupoid (->) where + compose f g x = f (g x) + +instance categoryFn :: Category (->) where + identity x = x + +test :: forall a. a -> a +test = identity + +test2 :: Int -> Int +test2 = identity diff --git a/tests-integration/fixtures/checking/236_category_function_instance/Main.snap b/tests-integration/fixtures/checking/236_category_function_instance/Main.snap new file mode 100644 index 00000000..a1b96504 --- /dev/null +++ b/tests-integration/fixtures/checking/236_category_function_instance/Main.snap @@ -0,0 +1,33 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +compose :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) (b :: (k :: Type)) (c :: (k :: Type)) + (d :: (k :: Type)). + Semigroupoid (a :: (k :: Type) -> (k :: Type) -> Type) => + (a :: (k :: Type) -> (k :: Type) -> Type) (c :: (k :: Type)) (d :: (k :: Type)) -> + (a :: (k :: Type) -> (k :: Type) -> Type) (b :: (k :: Type)) (c :: (k :: Type)) -> + (a :: (k :: Type) -> (k :: Type) -> Type) (b :: (k :: Type)) (d :: (k :: Type)) +identity :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)). + Category (a :: (k :: Type) -> (k :: Type) -> Type) => + (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)) (t :: (k :: Type)) +test :: forall (a :: Type). (a :: Type) -> (a :: Type) +test2 :: Int -> Int + +Types +Semigroupoid :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint +Category :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint + +Classes +class Semigroupoid (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) +class Semigroupoid @(&0 :: Type) (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) <= Category (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) + +Instances +instance Semigroupoid (Function :: Type -> Type -> Type) + chain: 0 +instance Category (Function :: Type -> Type -> Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index c79a4c64..4c6832f0 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -497,3 +497,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_234_record_instance_open_row_main() { run_test("234_record_instance_open_row", "Main"); } #[rustfmt::skip] #[test] fn test_235_instance_head_invalid_row_main() { run_test("235_instance_head_invalid_row", "Main"); } + +#[rustfmt::skip] #[test] fn test_236_category_function_instance_main() { run_test("236_category_function_instance", "Main"); } From 263346f569dc49f149b7075fc8b07b3bf5d0d22f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 27 Jan 2026 03:25:23 +0800 Subject: [PATCH 044/386] Add missing unification rule for Type::Variable This adds a missing unification rule for Type::Variable, which relied on an ID-based check before inline kinds were introduced. --- .../checking/src/algorithm/unification.rs | 22 ++++++++++++++++ .../237_bound_variable_unification/Main.purs | 20 +++++++++++++++ .../237_bound_variable_unification/Main.snap | 25 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 69 insertions(+) create mode 100644 tests-integration/fixtures/checking/237_bound_variable_unification/Main.purs create mode 100644 tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index add20506..143902ad 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -200,6 +200,28 @@ where (Type::Row(t1_row), Type::Row(t2_row)) => unify_rows(state, context, t1_row, t2_row)?, + ( + Type::Variable(Variable::Bound(t1_level, t1_kind)), + Type::Variable(Variable::Bound(t2_level, t2_kind)), + ) => { + if t1_level == t2_level { + unify(state, context, t1_kind, t2_kind)? + } else { + false + } + } + + ( + Type::Variable(Variable::Skolem(t1_level, t1_kind)), + Type::Variable(Variable::Skolem(t2_level, t2_kind)), + ) => { + if t1_level == t2_level { + unify(state, context, t1_kind, t2_kind)? + } else { + false + } + } + (Type::Unification(unification_id), _) => { solve(state, context, unification_id, t2)?.is_some() } diff --git a/tests-integration/fixtures/checking/237_bound_variable_unification/Main.purs b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.purs new file mode 100644 index 00000000..83638e68 --- /dev/null +++ b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.purs @@ -0,0 +1,20 @@ +module Main where + +-- Test based on Data.Bounded's boundedRecordCons pattern where +-- a bound row variable appears in a type annotation within a where clause. + +foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2 + +class BuildRecord :: Row Type -> Row Type -> Constraint +class BuildRecord row subrow | row -> subrow where + buildIt :: Record subrow + +instance buildRecordImpl :: BuildRecord row subrow where + buildIt = result + where + -- Type annotation references the bound variable `subrow` + result :: Record subrow + result = unsafeSet "x" 42 {} + +test :: forall r s. BuildRecord r s => Record s +test = buildIt diff --git a/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap new file mode 100644 index 00000000..c4d5dadb --- /dev/null +++ b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeSet :: + forall (r1 :: Row Type) (r2 :: Row Type) (a :: Type). + String -> (a :: Type) -> {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } +buildIt :: + forall (row :: Row Type) (subrow :: Row Type). + BuildRecord (row :: Row Type) (subrow :: Row Type) => {| (subrow :: Row Type) } +test :: + forall (r :: Row Type) (s :: Row Type). + BuildRecord (r :: Row Type) (s :: Row Type) => {| (s :: Row Type) } + +Types +BuildRecord :: Row Type -> Row Type -> Constraint + +Classes +class BuildRecord (&0 :: Row Type) (&1 :: Row Type) + +Instances +instance BuildRecord ((&0 :: Row Type) :: Row Type) ((&1 :: Row Type) :: Row Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 4c6832f0..7e8fcc5b 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -499,3 +499,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_235_instance_head_invalid_row_main() { run_test("235_instance_head_invalid_row", "Main"); } #[rustfmt::skip] #[test] fn test_236_category_function_instance_main() { run_test("236_category_function_instance", "Main"); } + +#[rustfmt::skip] #[test] fn test_237_bound_variable_unification_main() { run_test("237_bound_variable_unification", "Main"); } From c215bba0083d84d2048369e45e124394d8060b7d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 27 Jan 2026 03:35:06 +0800 Subject: [PATCH 045/386] Add subtyping rule for application-based Function --- .../checking/src/algorithm/unification.rs | 22 +++++++++++++++++++ .../Main.purs | 14 ++++++++++++ .../Main.snap | 22 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 60 insertions(+) create mode 100644 tests-integration/fixtures/checking/238_function_application_subtype/Main.purs create mode 100644 tests-integration/fixtures/checking/238_function_application_subtype/Main.snap diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 143902ad..ace49113 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -71,6 +71,28 @@ where && subtype(state, context, t1_result, t2_result)?) } + (Type::Application(t1_partial, t1_result), Type::Function(t2_argument, t2_result)) => { + let t1_partial = state.normalize_type(t1_partial); + if let Type::Application(t1_constructor, t1_argument) = state.storage[t1_partial] { + Ok(unify(state, context, t1_constructor, context.prim.function)? + && subtype(state, context, t2_argument, t1_argument)? + && subtype(state, context, t1_result, t2_result)?) + } else { + unify(state, context, t1, t2) + } + } + + (Type::Function(t1_argument, t1_result), Type::Application(t2_partial, t2_result)) => { + let t2_partial = state.normalize_type(t2_partial); + if let Type::Application(t2_constructor, t2_argument) = state.storage[t2_partial] { + Ok(unify(state, context, t2_constructor, context.prim.function)? + && subtype(state, context, t2_argument, t1_argument)? + && subtype(state, context, t1_result, t2_result)?) + } else { + unify(state, context, t1, t2) + } + } + (_, Type::Forall(ref binder, inner)) => { let v = Variable::Skolem(binder.level, binder.kind); let t = state.storage.intern(Type::Variable(v)); diff --git a/tests-integration/fixtures/checking/238_function_application_subtype/Main.purs b/tests-integration/fixtures/checking/238_function_application_subtype/Main.purs new file mode 100644 index 00000000..0354dab6 --- /dev/null +++ b/tests-integration/fixtures/checking/238_function_application_subtype/Main.purs @@ -0,0 +1,14 @@ +module Main where + +class Category :: forall k. (k -> k -> Type) -> Constraint +class Category a where + identity :: forall t. a t t + +instance categoryFn :: Category (->) where + identity x = x + +testFnApp :: (->) Int Int +testFnApp = identity + +testAppFn :: Int -> Int +testAppFn = identity diff --git a/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap b/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap new file mode 100644 index 00000000..eee88fc1 --- /dev/null +++ b/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +identity :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)). + Category (a :: (k :: Type) -> (k :: Type) -> Type) => + (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)) (t :: (k :: Type)) +testFnApp :: Function Int Int +testAppFn :: Int -> Int + +Types +Category :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint + +Classes +class Category (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) + +Instances +instance Category (Function :: Type -> Type -> Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 7e8fcc5b..de5594fa 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -501,3 +501,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_236_category_function_instance_main() { run_test("236_category_function_instance", "Main"); } #[rustfmt::skip] #[test] fn test_237_bound_variable_unification_main() { run_test("237_bound_variable_unification", "Main"); } + +#[rustfmt::skip] #[test] fn test_238_function_application_subtype_main() { run_test("238_function_application_subtype", "Main"); } From a5f81bd589733ad427a89dbfda5a9d3f6fe4ba95 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 27 Jan 2026 18:43:38 +0800 Subject: [PATCH 046/386] Fix panic on single-statement do expressions --- compiler-core/checking/src/algorithm/term.rs | 10 +++++---- .../checking/245_do_notation_panic/Main.purs | 15 +++++++++++++ .../checking/245_do_notation_panic/Main.snap | 21 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests-integration/fixtures/checking/245_do_notation_panic/Main.purs create mode 100644 tests-integration/fixtures/checking/245_do_notation_panic/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index fec76537..cd5b1cda 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -791,10 +791,12 @@ where // The `first_continuation` is the overall type of the do expression, // built iteratively and through solving unification variables. On // the other hand, the `final_continuation` is the expected type for - // the final statement in the do expression. - let [first_continuation, .., final_continuation] = continuation_types[..] else { - unreachable!("invariant violated: insufficient continuation_types"); - }; + // the final statement in the do expression. If there is only a single + // statement in the do expression, then these two bindings are equivalent. + let first_continuation = + *continuation_types.first().expect("invariant violated: empty continuation_types"); + let final_continuation = + *continuation_types.last().expect("invariant violated: empty continuation_types"); check_expression(state, context, pure_expression, final_continuation)?; diff --git a/tests-integration/fixtures/checking/245_do_notation_panic/Main.purs b/tests-integration/fixtures/checking/245_do_notation_panic/Main.purs new file mode 100644 index 00000000..9b3a9d8c --- /dev/null +++ b/tests-integration/fixtures/checking/245_do_notation_panic/Main.purs @@ -0,0 +1,15 @@ +module Main where + +foreign import data Effect :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +-- Single-statement do block should work without panicking +test :: Effect Int +test = do + pure 1 + +test' = do + pure 1 diff --git a/tests-integration/fixtures/checking/245_do_notation_panic/Main.snap b/tests-integration/fixtures/checking/245_do_notation_panic/Main.snap new file mode 100644 index 00000000..c5f72261 --- /dev/null +++ b/tests-integration/fixtures/checking/245_do_notation_panic/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +test :: Effect Int +test' :: Effect Int + +Types +Effect :: Type -> Type + +Roles +Effect = [Nominal] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index de5594fa..fc9c2450 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -503,3 +503,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_237_bound_variable_unification_main() { run_test("237_bound_variable_unification", "Main"); } #[rustfmt::skip] #[test] fn test_238_function_application_subtype_main() { run_test("238_function_application_subtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_245_do_notation_panic_main() { run_test("245_do_notation_panic", "Main"); } From d18813a7832885722972700cb0c17bf75b76c1e6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 27 Jan 2026 20:26:13 +0800 Subject: [PATCH 047/386] Allow discard to be resolved lazily This fixes an issue where `discard` is eagerly required when checking do expressions, even on expressions that only ever use `bind`. This lazy resolution is necessary as `bind` and `discard` are canonically defined in separate modules, `Control.Bind` and `Control.Discard`. In addition to lazy resolution, if `discard` cannot be resolved, the compiler synthesises a type that would match most of its general usages, outside of non-monadic definitions of `bind` and `discard`. --- compiler-core/checking/src/algorithm/term.rs | 28 +++++++++++-- .../lowering/src/algorithm/recursive.rs | 39 ++++++++++++------- .../checking/053_do_polymorphic/Main.snap | 2 +- .../checking/117_do_ado_constrained/Main.snap | 10 ++--- .../checking/217_do_monad_error/Main.snap | 6 +-- .../219_do_mixed_monad_error/Main.snap | 6 +-- .../checking/246_do_bind_only/Main.purs | 14 +++++++ .../checking/246_do_bind_only/Main.snap | 12 ++++++ .../247_do_discard_not_in_scope/Main.purs | 16 ++++++++ .../247_do_discard_not_in_scope/Main.snap | 24 ++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 11 files changed, 132 insertions(+), 29 deletions(-) create mode 100644 tests-integration/fixtures/checking/246_do_bind_only/Main.purs create mode 100644 tests-integration/fixtures/checking/246_do_bind_only/Main.snap create mode 100644 tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.purs create mode 100644 tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index cd5b1cda..72c13862 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -423,7 +423,6 @@ where lowering::ExpressionKind::Do { bind, discard, statements } => { let Some(bind) = bind else { return Ok(unknown) }; - let Some(discard) = discard else { return Ok(unknown) }; infer_do(state, context, *bind, *discard, statements) } @@ -613,14 +612,37 @@ fn infer_do( state: &mut CheckState, context: &CheckContext, bind: lowering::TermVariableResolution, - discard: lowering::TermVariableResolution, + discard: Option, statement_ids: &[lowering::DoStatementId], ) -> QueryResult where Q: ExternalQueries, { let bind_type = lookup_term_variable(state, context, bind)?; - let discard_type = lookup_term_variable(state, context, discard)?; + + let discard_type = if let Some(discard) = discard { + lookup_term_variable(state, context, discard)? + } else { + // TODO: Consider reporting this synthetic discard type once these + // unification variables have been solved to concrete types. This + // would be a nice addition to the default 'discard' is not in scope + // errors where we can show the inferred type for this specific do + // expression. This would not happen often, but it's nice UX + // + // TODO: Consider doing the same for `bind`, `map`, `pure`, and `apply` + // + // This default assumes that `discard` is defined in a shape that + // would unify against `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. This could + // mean that places that expect non-monadic versions of `discard` + // would suffer, but for now this is a good default for the error path. + let m = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let m_a = state.storage.intern(Type::Application(m, a)); + let m_b = state.storage.intern(Type::Application(m, b)); + let a_to_m_b = state.storage.intern(Type::Function(a, m_b)); + state.make_function(&[m_a, a_to_m_b], m_b) + }; // First, perform a forward pass where variable bindings are bound // to unification variables. Let bindings are not checked here to diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index e5231086..9b973765 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -278,22 +278,33 @@ fn lower_expression_kind( Some(SmolStr::from(text)) }); - let bind = state.resolve_term_full(context, qualifier.as_deref(), "bind"); - let discard = state.resolve_term_full(context, qualifier.as_deref(), "discard"); + let has_discard = cst.statements().is_some_and(|statements| { + let mut statements = statements.children().peekable(); + while let Some(statement) = statements.next() { + if matches!(statement, cst::DoStatement::DoStatementDiscard(_)) + && statements.peek().is_some() + { + return true; + } + } + false + }); - if bind.is_none() { - let id = context.stabilized.lookup_cst(cst).expect_id(); - state - .errors - .push(LoweringError::NotInScope(NotInScope::DoFn { kind: DoFn::Bind, id })); - } + let mut resolve_do_fn = |kind: DoFn| { + let name = match kind { + DoFn::Bind => "bind", + DoFn::Discard => "discard", + }; + let resolution = state.resolve_term_full(context, qualifier.as_deref(), name); + if resolution.is_none() { + let id = context.stabilized.lookup_cst(cst).expect_id(); + state.errors.push(LoweringError::NotInScope(NotInScope::DoFn { kind, id })); + } + resolution + }; - if discard.is_none() { - let id = context.stabilized.lookup_cst(cst).expect_id(); - state - .errors - .push(LoweringError::NotInScope(NotInScope::DoFn { kind: DoFn::Discard, id })); - } + let bind = resolve_do_fn(DoFn::Bind); + let discard = if has_discard { resolve_do_fn(DoFn::Discard) } else { None }; let statements = recover! { cst.statements()? diff --git a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap index 9de9d422..454a7354 100644 --- a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap @@ -17,7 +17,7 @@ discard :: (m :: Type -> Type) (b :: Type) pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) test :: forall (m :: Type -> Type). (m :: Type -> Type) (Tuple Int String) -test' :: forall (t55 :: Type -> Type). (t55 :: Type -> Type) (Tuple Int String) +test' :: forall (t61 :: Type -> Type). (t61 :: Type -> Type) (Tuple Int String) Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 16e6a030..eccd2f6d 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -35,17 +35,17 @@ bind :: testDo :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) (Tuple Int String) testDo' :: - forall (t52 :: Type -> Type). - Bind (t52 :: Type -> Type) => (t52 :: Type -> Type) (Tuple Int String) + forall (t58 :: Type -> Type). + Bind (t58 :: Type -> Type) => (t58 :: Type -> Type) (Tuple Int String) testAdo :: forall (f :: Type -> Type). Applicative (f :: Type -> Type) => (f :: Type -> Type) (Tuple Int String) testAdo' :: - forall (t91 :: Type -> Type). - Applicative (t91 :: Type -> Type) => (t91 :: Type -> Type) (Tuple Int String) + forall (t97 :: Type -> Type). + Applicative (t97 :: Type -> Type) => (t97 :: Type -> Type) (Tuple Int String) testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int testDoDiscard' :: - forall (t111 :: Type -> Type). Discard (t111 :: Type -> Type) => (t111 :: Type -> Type) Int + forall (t117 :: Type -> Type). Discard (t117 :: Type -> Type) => (t117 :: Type -> Type) Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 252b65fa..6e280913 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -13,7 +13,7 @@ discard :: forall (a :: Type) (b :: Type). Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } -test' :: forall (t27 :: Type). Effect { a :: Int, b :: (t27 :: Type), c :: Int } +test' :: forall (t33 :: Type). Effect { a :: Int, b :: (t33 :: Type), c :: Int } Types Effect :: Type -> Type @@ -29,7 +29,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 15 | b <- affPure "life" | ^~~~~~~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?18[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?21[:0]' --> 15:3..15:22 | 15 | b <- affPure "life" @@ -39,7 +39,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 21 | b <- affPure "life" | ^~~~~~~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?37[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?43[:0]' --> 21:3..21:22 | 21 | b <- affPure "life" diff --git a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap index e1290c6f..35dc6cec 100644 --- a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap @@ -7,7 +7,7 @@ Terms effect :: Effect Int aff :: Aff String test :: Effect { a :: Int, b :: String } -test' :: forall (t23 :: Type). Effect (t23 :: Type) +test' :: forall (t29 :: Type). Effect (t29 :: Type) Types @@ -17,7 +17,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 16 | b <- aff | ^~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff ?11[:0]' with 'Effect ?8[:0]' +error[CannotUnify]: Cannot unify 'Aff ?14[:0]' with 'Effect ?11[:0]' --> 16:3..16:11 | 16 | b <- aff @@ -27,7 +27,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 22 | b <- aff | ^~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff ?26[:0]' with 'Effect ?23[:0]' +error[CannotUnify]: Cannot unify 'Aff ?32[:0]' with 'Effect ?29[:0]' --> 22:3..22:11 | 22 | b <- aff diff --git a/tests-integration/fixtures/checking/246_do_bind_only/Main.purs b/tests-integration/fixtures/checking/246_do_bind_only/Main.purs new file mode 100644 index 00000000..2e40cc8e --- /dev/null +++ b/tests-integration/fixtures/checking/246_do_bind_only/Main.purs @@ -0,0 +1,14 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind) +import Effect (Effect) + +test :: Effect Int +test = do + x <- pure 1 + pure x + +test' = do + x <- pure 1 + pure x diff --git a/tests-integration/fixtures/checking/246_do_bind_only/Main.snap b/tests-integration/fixtures/checking/246_do_bind_only/Main.snap new file mode 100644 index 00000000..4e4be31f --- /dev/null +++ b/tests-integration/fixtures/checking/246_do_bind_only/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: + forall (t23 :: Type -> Type). + Bind (t23 :: Type -> Type) => Applicative (t23 :: Type -> Type) => (t23 :: Type -> Type) Int + +Types diff --git a/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.purs b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.purs new file mode 100644 index 00000000..ee5abbee --- /dev/null +++ b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.purs @@ -0,0 +1,16 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind) +import Effect (Effect) + +test :: Effect Int +test = do + pure 0 + x <- pure 1 + pure x + +test' = do + pure 0 + x <- pure 1 + pure x diff --git a/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap new file mode 100644 index 00000000..4b448246 --- /dev/null +++ b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: + forall (t18 :: Type -> Type). + Applicative (t18 :: Type -> Type) => Bind (t18 :: Type -> Type) => (t18 :: Type -> Type) Int + +Types + +Diagnostics +error[NotInScope]: 'discard' is not in scope + --> 8:8..11:9 + | +8 | test = do + | ^~ +error[NotInScope]: 'discard' is not in scope + --> 13:9..16:9 + | +13 | test' = do + | ^~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index fc9c2450..15db4893 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -505,3 +505,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_238_function_application_subtype_main() { run_test("238_function_application_subtype", "Main"); } #[rustfmt::skip] #[test] fn test_245_do_notation_panic_main() { run_test("245_do_notation_panic", "Main"); } + +#[rustfmt::skip] #[test] fn test_246_do_bind_only_main() { run_test("246_do_bind_only", "Main"); } + +#[rustfmt::skip] #[test] fn test_247_do_discard_not_in_scope_main() { run_test("247_do_discard_not_in_scope", "Main"); } From 123c3cd3e906ee600db4e537b49187d4d9029c9e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 03:01:44 +0800 Subject: [PATCH 048/386] Defer resolution for bind, map, apply, pure to usage positions Extends the lazy discard pattern from d18813a7 to all rebindable do/ado functions. Resolution and error reporting now only occurs when the function is actually needed: Do-notation: - bind: only when <- statements exist - discard: only when non-final discard statements exist (unchanged) Ado-notation: - 0 actions: only pure needed - 1 action: only map needed - 2+ actions: map + apply needed When a needed function is missing, a constraint-free synthetic type is used to allow type checking to continue with useful inference. This improves error recovery and avoids spurious 'not in scope' errors for unused functions. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019bffd6-adf2-777c-bfdb-dcc91b5b504f --- compiler-core/checking/src/algorithm/term.rs | 161 +++++++++++++----- .../lowering/src/algorithm/recursive.rs | 80 ++++++--- .../checking/217_do_monad_error/Main.snap | 2 +- .../247_do_discard_not_in_scope/Main.snap | 4 +- .../lowering__ado_fn_not_in_scope.snap | 13 +- .../lowering__do_fn_not_in_scope.snap | 13 +- 6 files changed, 180 insertions(+), 93 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 72c13862..b45e1fa3 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -422,14 +422,10 @@ where } lowering::ExpressionKind::Do { bind, discard, statements } => { - let Some(bind) = bind else { return Ok(unknown) }; infer_do(state, context, *bind, *discard, statements) } lowering::ExpressionKind::Ado { map, apply, pure, statements, expression } => { - let Some(map) = map else { return Ok(unknown) }; - let Some(apply) = apply else { return Ok(unknown) }; - let Some(pure) = pure else { return Ok(unknown) }; infer_ado(state, context, *map, *apply, *pure, statements, *expression) } @@ -607,43 +603,80 @@ where Ok(inferred_type) } +/// Synthesize a constraint-free type for `bind`: `?m ?a -> (?a -> ?m ?b) -> ?m ?b` +fn synth_bind_type(state: &mut CheckState, context: &CheckContext) -> TypeId +where + Q: ExternalQueries, +{ + let m = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let m_a = state.storage.intern(Type::Application(m, a)); + let m_b = state.storage.intern(Type::Application(m, b)); + let a_to_m_b = state.storage.intern(Type::Function(a, m_b)); + state.make_function(&[m_a, a_to_m_b], m_b) +} + +/// Synthesize a constraint-free type for `discard`: `?m ?a -> (?a -> ?m ?b) -> ?m ?b` +fn synth_discard_type(state: &mut CheckState, context: &CheckContext) -> TypeId +where + Q: ExternalQueries, +{ + // Same shape as bind + synth_bind_type(state, context) +} + +/// Synthesize a constraint-free type for `map`: `(?a -> ?b) -> ?f ?a -> ?f ?b` +fn synth_map_type(state: &mut CheckState, context: &CheckContext) -> TypeId +where + Q: ExternalQueries, +{ + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let f_a = state.storage.intern(Type::Application(f, a)); + let f_b = state.storage.intern(Type::Application(f, b)); + let a_to_b = state.storage.intern(Type::Function(a, b)); + state.make_function(&[a_to_b, f_a], f_b) +} + +/// Synthesize a constraint-free type for `apply`: `?f (?a -> ?b) -> ?f ?a -> ?f ?b` +fn synth_apply_type(state: &mut CheckState, context: &CheckContext) -> TypeId +where + Q: ExternalQueries, +{ + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let a_to_b = state.storage.intern(Type::Function(a, b)); + let f_a_to_b = state.storage.intern(Type::Application(f, a_to_b)); + let f_a = state.storage.intern(Type::Application(f, a)); + let f_b = state.storage.intern(Type::Application(f, b)); + state.make_function(&[f_a_to_b, f_a], f_b) +} + +/// Synthesize a constraint-free type for `pure`: `?a -> ?f ?a` +fn synth_pure_type(state: &mut CheckState, context: &CheckContext) -> TypeId +where + Q: ExternalQueries, +{ + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let f_a = state.storage.intern(Type::Application(f, a)); + state.storage.intern(Type::Function(a, f_a)) +} + #[tracing::instrument(skip_all, name = "infer_do")] fn infer_do( state: &mut CheckState, context: &CheckContext, - bind: lowering::TermVariableResolution, + bind: Option, discard: Option, statement_ids: &[lowering::DoStatementId], ) -> QueryResult where Q: ExternalQueries, { - let bind_type = lookup_term_variable(state, context, bind)?; - - let discard_type = if let Some(discard) = discard { - lookup_term_variable(state, context, discard)? - } else { - // TODO: Consider reporting this synthetic discard type once these - // unification variables have been solved to concrete types. This - // would be a nice addition to the default 'discard' is not in scope - // errors where we can show the inferred type for this specific do - // expression. This would not happen often, but it's nice UX - // - // TODO: Consider doing the same for `bind`, `map`, `pure`, and `apply` - // - // This default assumes that `discard` is defined in a shape that - // would unify against `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. This could - // mean that places that expect non-monadic versions of `discard` - // would suffer, but for now this is a good default for the error path. - let m = state.fresh_unification_kinded(context.prim.type_to_type); - let a = state.fresh_unification_type(context); - let b = state.fresh_unification_type(context); - let m_a = state.storage.intern(Type::Application(m, a)); - let m_b = state.storage.intern(Type::Application(m, b)); - let a_to_m_b = state.storage.intern(Type::Function(a, m_b)); - state.make_function(&[m_a, a_to_m_b], m_b) - }; - // First, perform a forward pass where variable bindings are bound // to unification variables. Let bindings are not checked here to // avoid premature solving of unification variables. Instead, they @@ -680,6 +713,33 @@ where .filter(|step| matches!(step, DoStep::Bind { .. } | DoStep::Discard { .. })) .count(); + // Lazily compute bind_type and discard_type only when needed. + // This avoids creating unused unification variables. + let has_bind_step = steps.iter().any(|s| matches!(s, DoStep::Bind { .. })); + let has_discard_step = steps.iter().any(|s| matches!(s, DoStep::Discard { .. })); + + let bind_type = if has_bind_step { + if let Some(bind) = bind { + lookup_term_variable(state, context, bind)? + } else { + synth_bind_type(state, context) + } + } else { + // Not needed, use a dummy that won't be accessed + context.prim.unknown + }; + + let discard_type = if has_discard_step { + if let Some(discard) = discard { + lookup_term_variable(state, context, discard)? + } else { + synth_discard_type(state, context) + } + } else { + // Not needed, use a dummy that won't be accessed + context.prim.unknown + }; + let pure_expression = steps.iter().rev().find_map(|step| match step { DoStep::Bind { expression, .. } | DoStep::Discard { expression, .. } => Some(*expression), DoStep::Let { .. } => None, @@ -857,19 +917,15 @@ enum AdoStep<'a> { fn infer_ado( state: &mut CheckState, context: &CheckContext, - map: lowering::TermVariableResolution, - apply: lowering::TermVariableResolution, - pure: lowering::TermVariableResolution, + map: Option, + apply: Option, + pure: Option, statement_ids: &[lowering::DoStatementId], expression: Option, ) -> QueryResult where Q: ExternalQueries, { - let map_type = lookup_term_variable(state, context, map)?; - let apply_type = lookup_term_variable(state, context, apply)?; - let pure_type = lookup_term_variable(state, context, pure)?; - // First, perform a forward pass where variable bindings are bound // to unification variables. Let bindings are not checked here to // avoid premature solving of unification variables. Instead, they @@ -921,6 +977,12 @@ where } } return if let Some(expression) = expression { + // Lazily compute pure_type only when needed (0 actions case) + let pure_type = if let Some(pure) = pure { + lookup_term_variable(state, context, pure)? + } else { + synth_pure_type(state, context) + }; check_function_term_application(state, context, pure_type, expression) } else { state.insert_error(ErrorKind::EmptyAdoBlock); @@ -964,6 +1026,29 @@ where // >> ?a := String // // continuation_type := Effect (?b -> ?in_expression) + + // Lazily compute map_type and apply_type only when needed. + // - 1 action: only map is needed + // - 2+ actions: map and apply are needed + let action_count = binder_types.len(); + + let map_type = if let Some(map) = map { + lookup_term_variable(state, context, map)? + } else { + synth_map_type(state, context) + }; + + let apply_type = if action_count > 1 { + if let Some(apply) = apply { + lookup_term_variable(state, context, apply)? + } else { + synth_apply_type(state, context) + } + } else { + // Not needed for single action, use a dummy + context.prim.unknown + }; + let mut continuation_type = None; for step in &steps { diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index 9b973765..4907ccc3 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -278,16 +278,23 @@ fn lower_expression_kind( Some(SmolStr::from(text)) }); - let has_discard = cst.statements().is_some_and(|statements| { + // Scan statements to determine which rebindable functions are needed: + // - `bind` is needed if there's at least one `<-` statement + // - `discard` is needed if there's a non-final discard statement + let (has_bind, has_discard) = cst.statements().map_or((false, false), |statements| { let mut statements = statements.children().peekable(); + let mut has_bind = false; + let mut has_discard = false; while let Some(statement) = statements.next() { - if matches!(statement, cst::DoStatement::DoStatementDiscard(_)) - && statements.peek().is_some() - { - return true; + match statement { + cst::DoStatement::DoStatementBind(_) => has_bind = true, + cst::DoStatement::DoStatementDiscard(_) if statements.peek().is_some() => { + has_discard = true + } + _ => {} } } - false + (has_bind, has_discard) }); let mut resolve_do_fn = |kind: DoFn| { @@ -303,7 +310,7 @@ fn lower_expression_kind( resolution }; - let bind = resolve_do_fn(DoFn::Bind); + let bind = if has_bind { resolve_do_fn(DoFn::Bind) } else { None }; let discard = if has_discard { resolve_do_fn(DoFn::Discard) } else { None }; let statements = recover! { @@ -322,30 +329,47 @@ fn lower_expression_kind( Some(SmolStr::from(text)) }); - let map = state.resolve_term_full(context, qualifier.as_deref(), "map"); - let apply = state.resolve_term_full(context, qualifier.as_deref(), "apply"); - let pure = state.resolve_term_full(context, qualifier.as_deref(), "pure"); + // Count action statements (Bind/Discard, ignoring Let) to determine + // which rebindable functions are needed: + // - 0 actions: only `pure` needed + // - 1 action: only `map` needed + // - 2+ actions: `map` and `apply` needed + let action_count = cst.statements().map_or(0, |statements| { + statements + .children() + .filter(|s| { + matches!( + s, + cst::DoStatement::DoStatementBind(_) + | cst::DoStatement::DoStatementDiscard(_) + ) + }) + .count() + }); - if map.is_none() { - let id = context.stabilized.lookup_cst(cst).expect_id(); - state - .errors - .push(LoweringError::NotInScope(NotInScope::AdoFn { kind: AdoFn::Map, id })); - } + let (needs_pure, needs_map, needs_apply) = match action_count { + 0 => (true, false, false), + 1 => (false, true, false), + _ => (false, true, true), + }; - if apply.is_none() { - let id = context.stabilized.lookup_cst(cst).expect_id(); - state - .errors - .push(LoweringError::NotInScope(NotInScope::AdoFn { kind: AdoFn::Apply, id })); - } + let mut resolve_ado_fn = |kind: AdoFn| { + let name = match kind { + AdoFn::Map => "map", + AdoFn::Apply => "apply", + AdoFn::Pure => "pure", + }; + let resolution = state.resolve_term_full(context, qualifier.as_deref(), name); + if resolution.is_none() { + let id = context.stabilized.lookup_cst(cst).expect_id(); + state.errors.push(LoweringError::NotInScope(NotInScope::AdoFn { kind, id })); + } + resolution + }; - if pure.is_none() { - let id = context.stabilized.lookup_cst(cst).expect_id(); - state - .errors - .push(LoweringError::NotInScope(NotInScope::AdoFn { kind: AdoFn::Pure, id })); - } + let map = if needs_map { resolve_ado_fn(AdoFn::Map) } else { None }; + let apply = if needs_apply { resolve_ado_fn(AdoFn::Apply) } else { None }; + let pure = if needs_pure { resolve_ado_fn(AdoFn::Pure) } else { None }; let statements = recover! { cst.statements()? diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index 6e280913..a7332e94 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -13,7 +13,7 @@ discard :: forall (a :: Type) (b :: Type). Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } -test' :: forall (t33 :: Type). Effect { a :: Int, b :: (t33 :: Type), c :: Int } +test' :: forall (t30 :: Type). Effect { a :: Int, b :: (t30 :: Type), c :: Int } Types Effect :: Type -> Type diff --git a/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap index 4b448246..b7408501 100644 --- a/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap +++ b/tests-integration/fixtures/checking/247_do_discard_not_in_scope/Main.snap @@ -6,8 +6,8 @@ expression: report Terms test :: Effect Int test' :: - forall (t18 :: Type -> Type). - Applicative (t18 :: Type -> Type) => Bind (t18 :: Type -> Type) => (t18 :: Type -> Type) Int + forall (t19 :: Type -> Type). + Applicative (t19 :: Type -> Type) => Bind (t19 :: Type -> Type) => (t19 :: Type -> Type) Int Types diff --git a/tests-integration/tests/snapshots/lowering__ado_fn_not_in_scope.snap b/tests-integration/tests/snapshots/lowering__ado_fn_not_in_scope.snap index 521cc22e..ac3aa405 100644 --- a/tests-integration/tests/snapshots/lowering__ado_fn_not_in_scope.snap +++ b/tests-integration/tests/snapshots/lowering__ado_fn_not_in_scope.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/lowering.rs +assertion_line: 186 expression: lowered.errors --- [ @@ -9,18 +10,6 @@ expression: lowered.errors id: AstId(8), }, ), - NotInScope( - AdoFn { - kind: Apply, - id: AstId(8), - }, - ), - NotInScope( - AdoFn { - kind: Pure, - id: AstId(8), - }, - ), NotInScope( ExprVariable { id: AstId(13), diff --git a/tests-integration/tests/snapshots/lowering__do_fn_not_in_scope.snap b/tests-integration/tests/snapshots/lowering__do_fn_not_in_scope.snap index 9ac58c73..48165d7b 100644 --- a/tests-integration/tests/snapshots/lowering__do_fn_not_in_scope.snap +++ b/tests-integration/tests/snapshots/lowering__do_fn_not_in_scope.snap @@ -1,20 +1,9 @@ --- source: tests-integration/tests/lowering.rs +assertion_line: 161 expression: lowered.errors --- [ - NotInScope( - DoFn { - kind: Bind, - id: AstId(8), - }, - ), - NotInScope( - DoFn { - kind: Discard, - id: AstId(8), - }, - ), NotInScope( ExprVariable { id: AstId(12), From c7711836537b71dcbee6579d9641af4b97e6b8a0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 03:22:47 +0800 Subject: [PATCH 049/386] Clean up rebindable do/ado function resolution - Rename synth_* to lookup_or_synthesise_* taking Option - Use with_position() for allocation-free final-position checks - Only synthesise bind/discard types for non-final actions - Reduces unification variable count in do-blocks Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c00d9-1df4-778e-80f0-14a15fd7abf4g --- compiler-core/checking/src/algorithm/term.rs | 163 ++++++++++-------- .../lowering/src/algorithm/recursive.rs | 12 +- .../checking/053_do_polymorphic/Main.snap | 2 +- .../checking/117_do_ado_constrained/Main.snap | 10 +- .../checking/217_do_monad_error/Main.snap | 6 +- .../219_do_mixed_monad_error/Main.snap | 6 +- .../checking/246_do_bind_only/Main.snap | 4 +- 7 files changed, 112 insertions(+), 91 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index b45e1fa3..d1e3c8a0 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -2,7 +2,7 @@ use std::iter; use building_types::QueryResult; use indexing::TermItemId; -use itertools::Itertools; +use itertools::{Itertools, Position}; use smol_str::SmolStr; use crate::ExternalQueries; @@ -603,67 +603,103 @@ where Ok(inferred_type) } -/// Synthesize a constraint-free type for `bind`: `?m ?a -> (?a -> ?m ?b) -> ?m ?b` -fn synth_bind_type(state: &mut CheckState, context: &CheckContext) -> TypeId +/// Lookup `bind` from resolution, or synthesize `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. +fn lookup_or_synthesise_bind( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult where Q: ExternalQueries, { - let m = state.fresh_unification_kinded(context.prim.type_to_type); - let a = state.fresh_unification_type(context); - let b = state.fresh_unification_type(context); - let m_a = state.storage.intern(Type::Application(m, a)); - let m_b = state.storage.intern(Type::Application(m, b)); - let a_to_m_b = state.storage.intern(Type::Function(a, m_b)); - state.make_function(&[m_a, a_to_m_b], m_b) + if let Some(resolution) = resolution { + lookup_term_variable(state, context, resolution) + } else { + let m = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let m_a = state.storage.intern(Type::Application(m, a)); + let m_b = state.storage.intern(Type::Application(m, b)); + let a_to_m_b = state.storage.intern(Type::Function(a, m_b)); + Ok(state.make_function(&[m_a, a_to_m_b], m_b)) + } } -/// Synthesize a constraint-free type for `discard`: `?m ?a -> (?a -> ?m ?b) -> ?m ?b` -fn synth_discard_type(state: &mut CheckState, context: &CheckContext) -> TypeId +/// Lookup `discard` from resolution, or synthesize `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. +fn lookup_or_synthesise_discard( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult where Q: ExternalQueries, { // Same shape as bind - synth_bind_type(state, context) + lookup_or_synthesise_bind(state, context, resolution) } -/// Synthesize a constraint-free type for `map`: `(?a -> ?b) -> ?f ?a -> ?f ?b` -fn synth_map_type(state: &mut CheckState, context: &CheckContext) -> TypeId +/// Lookup `map` from resolution, or synthesize `(?a -> ?b) -> ?f ?a -> ?f ?b`. +fn lookup_or_synthesise_map( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult where Q: ExternalQueries, { - let f = state.fresh_unification_kinded(context.prim.type_to_type); - let a = state.fresh_unification_type(context); - let b = state.fresh_unification_type(context); - let f_a = state.storage.intern(Type::Application(f, a)); - let f_b = state.storage.intern(Type::Application(f, b)); - let a_to_b = state.storage.intern(Type::Function(a, b)); - state.make_function(&[a_to_b, f_a], f_b) + if let Some(resolution) = resolution { + lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let f_a = state.storage.intern(Type::Application(f, a)); + let f_b = state.storage.intern(Type::Application(f, b)); + let a_to_b = state.storage.intern(Type::Function(a, b)); + Ok(state.make_function(&[a_to_b, f_a], f_b)) + } } -/// Synthesize a constraint-free type for `apply`: `?f (?a -> ?b) -> ?f ?a -> ?f ?b` -fn synth_apply_type(state: &mut CheckState, context: &CheckContext) -> TypeId +/// Lookup `apply` from resolution, or synthesize `?f (?a -> ?b) -> ?f ?a -> ?f ?b`. +fn lookup_or_synthesise_apply( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult where Q: ExternalQueries, { - let f = state.fresh_unification_kinded(context.prim.type_to_type); - let a = state.fresh_unification_type(context); - let b = state.fresh_unification_type(context); - let a_to_b = state.storage.intern(Type::Function(a, b)); - let f_a_to_b = state.storage.intern(Type::Application(f, a_to_b)); - let f_a = state.storage.intern(Type::Application(f, a)); - let f_b = state.storage.intern(Type::Application(f, b)); - state.make_function(&[f_a_to_b, f_a], f_b) + if let Some(resolution) = resolution { + lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let b = state.fresh_unification_type(context); + let a_to_b = state.storage.intern(Type::Function(a, b)); + let f_a_to_b = state.storage.intern(Type::Application(f, a_to_b)); + let f_a = state.storage.intern(Type::Application(f, a)); + let f_b = state.storage.intern(Type::Application(f, b)); + Ok(state.make_function(&[f_a_to_b, f_a], f_b)) + } } -/// Synthesize a constraint-free type for `pure`: `?a -> ?f ?a` -fn synth_pure_type(state: &mut CheckState, context: &CheckContext) -> TypeId +/// Lookup `pure` from resolution, or synthesize `?a -> ?f ?a`. +fn lookup_or_synthesise_pure( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult where Q: ExternalQueries, { - let f = state.fresh_unification_kinded(context.prim.type_to_type); - let a = state.fresh_unification_type(context); - let f_a = state.storage.intern(Type::Application(f, a)); - state.storage.intern(Type::Function(a, f_a)) + if let Some(resolution) = resolution { + lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification_kinded(context.prim.type_to_type); + let a = state.fresh_unification_type(context); + let f_a = state.storage.intern(Type::Application(f, a)); + Ok(state.storage.intern(Type::Function(a, f_a))) + } } #[tracing::instrument(skip_all, name = "infer_do")] @@ -713,30 +749,29 @@ where .filter(|step| matches!(step, DoStep::Bind { .. } | DoStep::Discard { .. })) .count(); - // Lazily compute bind_type and discard_type only when needed. - // This avoids creating unused unification variables. - let has_bind_step = steps.iter().any(|s| matches!(s, DoStep::Bind { .. })); - let has_discard_step = steps.iter().any(|s| matches!(s, DoStep::Discard { .. })); + let (has_bind_step, has_discard_step) = { + let mut has_bind = false; + let mut has_discard = false; + for (position, statement) in steps.iter().with_position() { + let is_final = matches!(position, Position::Last | Position::Only); + match statement { + DoStep::Bind { .. } => has_bind = true, + DoStep::Discard { .. } if !is_final => has_discard = true, + _ => (), + } + } + (has_bind, has_discard) + }; let bind_type = if has_bind_step { - if let Some(bind) = bind { - lookup_term_variable(state, context, bind)? - } else { - synth_bind_type(state, context) - } + lookup_or_synthesise_bind(state, context, bind)? } else { - // Not needed, use a dummy that won't be accessed context.prim.unknown }; let discard_type = if has_discard_step { - if let Some(discard) = discard { - lookup_term_variable(state, context, discard)? - } else { - synth_discard_type(state, context) - } + lookup_or_synthesise_discard(state, context, discard)? } else { - // Not needed, use a dummy that won't be accessed context.prim.unknown }; @@ -977,12 +1012,7 @@ where } } return if let Some(expression) = expression { - // Lazily compute pure_type only when needed (0 actions case) - let pure_type = if let Some(pure) = pure { - lookup_term_variable(state, context, pure)? - } else { - synth_pure_type(state, context) - }; + let pure_type = lookup_or_synthesise_pure(state, context, pure)?; check_function_term_application(state, context, pure_type, expression) } else { state.insert_error(ErrorKind::EmptyAdoBlock); @@ -1032,20 +1062,11 @@ where // - 2+ actions: map and apply are needed let action_count = binder_types.len(); - let map_type = if let Some(map) = map { - lookup_term_variable(state, context, map)? - } else { - synth_map_type(state, context) - }; + let map_type = lookup_or_synthesise_map(state, context, map)?; let apply_type = if action_count > 1 { - if let Some(apply) = apply { - lookup_term_variable(state, context, apply)? - } else { - synth_apply_type(state, context) - } + lookup_or_synthesise_apply(state, context, apply)? } else { - // Not needed for single action, use a dummy context.prim.unknown }; diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index 4907ccc3..ede1b49e 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -1,7 +1,7 @@ use std::mem; use std::sync::Arc; -use itertools::Itertools; +use itertools::{Itertools, Position}; use petgraph::algo::tarjan_scc; use rowan::ast::AstNode; use rustc_hash::FxHashMap; @@ -282,18 +282,18 @@ fn lower_expression_kind( // - `bind` is needed if there's at least one `<-` statement // - `discard` is needed if there's a non-final discard statement let (has_bind, has_discard) = cst.statements().map_or((false, false), |statements| { - let mut statements = statements.children().peekable(); let mut has_bind = false; let mut has_discard = false; - while let Some(statement) = statements.next() { + + for (position, statement) in statements.children().with_position() { + let is_final = matches!(position, Position::Last | Position::Only); match statement { cst::DoStatement::DoStatementBind(_) => has_bind = true, - cst::DoStatement::DoStatementDiscard(_) if statements.peek().is_some() => { - has_discard = true - } + cst::DoStatement::DoStatementDiscard(_) if !is_final => has_discard = true, _ => {} } } + (has_bind, has_discard) }); diff --git a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap index 454a7354..9de9d422 100644 --- a/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/053_do_polymorphic/Main.snap @@ -17,7 +17,7 @@ discard :: (m :: Type -> Type) (b :: Type) pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) test :: forall (m :: Type -> Type). (m :: Type -> Type) (Tuple Int String) -test' :: forall (t61 :: Type -> Type). (t61 :: Type -> Type) (Tuple Int String) +test' :: forall (t55 :: Type -> Type). (t55 :: Type -> Type) (Tuple Int String) Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index eccd2f6d..16e6a030 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -35,17 +35,17 @@ bind :: testDo :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) (Tuple Int String) testDo' :: - forall (t58 :: Type -> Type). - Bind (t58 :: Type -> Type) => (t58 :: Type -> Type) (Tuple Int String) + forall (t52 :: Type -> Type). + Bind (t52 :: Type -> Type) => (t52 :: Type -> Type) (Tuple Int String) testAdo :: forall (f :: Type -> Type). Applicative (f :: Type -> Type) => (f :: Type -> Type) (Tuple Int String) testAdo' :: - forall (t97 :: Type -> Type). - Applicative (t97 :: Type -> Type) => (t97 :: Type -> Type) (Tuple Int String) + forall (t91 :: Type -> Type). + Applicative (t91 :: Type -> Type) => (t91 :: Type -> Type) (Tuple Int String) testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int testDoDiscard' :: - forall (t117 :: Type -> Type). Discard (t117 :: Type -> Type) => (t117 :: Type -> Type) Int + forall (t111 :: Type -> Type). Discard (t111 :: Type -> Type) => (t111 :: Type -> Type) Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap index a7332e94..252b65fa 100644 --- a/tests-integration/fixtures/checking/217_do_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/217_do_monad_error/Main.snap @@ -13,7 +13,7 @@ discard :: forall (a :: Type) (b :: Type). Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) test :: Effect { a :: Int, b :: String, c :: Int } -test' :: forall (t30 :: Type). Effect { a :: Int, b :: (t30 :: Type), c :: Int } +test' :: forall (t27 :: Type). Effect { a :: Int, b :: (t27 :: Type), c :: Int } Types Effect :: Type -> Type @@ -29,7 +29,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 15 | b <- affPure "life" | ^~~~~~~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?21[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?18[:0]' --> 15:3..15:22 | 15 | b <- affPure "life" @@ -39,7 +39,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 21 | b <- affPure "life" | ^~~~~~~~~~~~~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?43[:0]' +error[CannotUnify]: Cannot unify 'Aff String' with 'Effect ?37[:0]' --> 21:3..21:22 | 21 | b <- affPure "life" diff --git a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap index 35dc6cec..e1290c6f 100644 --- a/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap +++ b/tests-integration/fixtures/checking/219_do_mixed_monad_error/Main.snap @@ -7,7 +7,7 @@ Terms effect :: Effect Int aff :: Aff String test :: Effect { a :: Int, b :: String } -test' :: forall (t29 :: Type). Effect (t29 :: Type) +test' :: forall (t23 :: Type). Effect (t23 :: Type) Types @@ -17,7 +17,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 16 | b <- aff | ^~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff ?14[:0]' with 'Effect ?11[:0]' +error[CannotUnify]: Cannot unify 'Aff ?11[:0]' with 'Effect ?8[:0]' --> 16:3..16:11 | 16 | b <- aff @@ -27,7 +27,7 @@ error[CannotUnify]: Cannot unify 'Aff' with 'Effect' | 22 | b <- aff | ^~~~~~~~ -error[CannotUnify]: Cannot unify 'Aff ?32[:0]' with 'Effect ?29[:0]' +error[CannotUnify]: Cannot unify 'Aff ?26[:0]' with 'Effect ?23[:0]' --> 22:3..22:11 | 22 | b <- aff diff --git a/tests-integration/fixtures/checking/246_do_bind_only/Main.snap b/tests-integration/fixtures/checking/246_do_bind_only/Main.snap index 4e4be31f..44fed64f 100644 --- a/tests-integration/fixtures/checking/246_do_bind_only/Main.snap +++ b/tests-integration/fixtures/checking/246_do_bind_only/Main.snap @@ -6,7 +6,7 @@ expression: report Terms test :: Effect Int test' :: - forall (t23 :: Type -> Type). - Bind (t23 :: Type -> Type) => Applicative (t23 :: Type -> Type) => (t23 :: Type -> Type) Int + forall (t17 :: Type -> Type). + Bind (t17 :: Type -> Type) => Applicative (t17 :: Type -> Type) => (t17 :: Type -> Type) Int Types From ef6de72da8d124df490285802f0f1e1ebd5de767 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 03:42:35 +0800 Subject: [PATCH 050/386] Add diagnostics for invalid final bind and final let This adds the InvalidFinalBind warning and InvalidFinalLet error to inform the user that the final statement in a do expression is invalid. For bind statements, the expression can be used to recover the type of the do expression had it been a discard statement instead. Co-authored-by: Amp Amp-Thread: https://ampcode.com/threads/T-019c00ee-da00-73df-b838-016a21109a08 --- compiler-core/checking/src/algorithm/term.rs | 31 +++++++++++++++--- compiler-core/checking/src/error.rs | 2 ++ compiler-core/diagnostics/src/convert.rs | 10 ++++++ .../checking/248_do_empty_block/Main.purs | 8 +++++ .../checking/248_do_empty_block/Main.snap | 32 +++++++++++++++++++ .../checking/249_do_final_bind/Main.purs | 12 +++++++ .../checking/249_do_final_bind/Main.snap | 22 +++++++++++++ .../checking/250_do_final_let/Main.purs | 10 ++++++ .../checking/250_do_final_let/Main.snap | 32 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 6 ++++ 10 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 tests-integration/fixtures/checking/248_do_empty_block/Main.purs create mode 100644 tests-integration/fixtures/checking/248_do_empty_block/Main.snap create mode 100644 tests-integration/fixtures/checking/249_do_final_bind/Main.purs create mode 100644 tests-integration/fixtures/checking/249_do_final_bind/Main.snap create mode 100644 tests-integration/fixtures/checking/250_do_final_let/Main.purs create mode 100644 tests-integration/fixtures/checking/250_do_final_let/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index d1e3c8a0..83f234d0 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -775,12 +775,33 @@ where context.prim.unknown }; - let pure_expression = steps.iter().rev().find_map(|step| match step { - DoStep::Bind { expression, .. } | DoStep::Discard { expression, .. } => Some(*expression), - DoStep::Let { .. } => None, - }); + let pure_expression = match steps.iter().last() { + Some(statement) => match statement { + // Technically valid, syntactically disallowed. This allows + // partially-written do expressions to infer, with a friendly + // warning to nudge the user that `bind` is prohibited. + DoStep::Bind { statement, expression, .. } => { + state.with_error_step(ErrorStep::InferringDoBind(*statement), |state| { + state.insert_error(ErrorKind::InvalidFinalBind); + }); + expression + } + DoStep::Discard { expression, .. } => expression, + DoStep::Let { statement, .. } => { + state.with_error_step(ErrorStep::CheckingDoLet(*statement), |state| { + state.insert_error(ErrorKind::InvalidFinalLet); + }); + return Ok(context.prim.unknown); + } + }, + None => { + state.insert_error(ErrorKind::EmptyDoBlock); + return Ok(context.prim.unknown); + } + }; - let Some(Some(pure_expression)) = pure_expression else { + // If either don't actually have expressions, it's empty! + let Some(pure_expression) = *pure_expression else { state.insert_error(ErrorKind::EmptyDoBlock); return Ok(context.prim.unknown); }; diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 6b909107..128fb3ce 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -63,6 +63,8 @@ pub enum ErrorKind { DeriveMissingFunctor, EmptyAdoBlock, EmptyDoBlock, + InvalidFinalBind, + InvalidFinalLet, InstanceHeadMismatch { class_file: files::FileId, class_item: indexing::TypeItemId, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 7c2b9870..5c22a48c 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -216,6 +216,16 @@ impl ToDiagnostics for CheckError { ErrorKind::EmptyDoBlock => { (Severity::Error, "EmptyDoBlock", "Empty do block".to_string()) } + ErrorKind::InvalidFinalBind => ( + Severity::Warning, + "InvalidFinalBind", + "Invalid final bind statement in do expression".to_string(), + ), + ErrorKind::InvalidFinalLet => ( + Severity::Error, + "InvalidFinalLet", + "Invalid final let statement in do expression".to_string(), + ), ErrorKind::InstanceHeadMismatch { expected, actual, .. } => ( Severity::Error, "InstanceHeadMismatch", diff --git a/tests-integration/fixtures/checking/248_do_empty_block/Main.purs b/tests-integration/fixtures/checking/248_do_empty_block/Main.purs new file mode 100644 index 00000000..fc2d819a --- /dev/null +++ b/tests-integration/fixtures/checking/248_do_empty_block/Main.purs @@ -0,0 +1,8 @@ +module Main where + +import Effect (Effect) + +test :: Effect Int +test = do + +test' = do diff --git a/tests-integration/fixtures/checking/248_do_empty_block/Main.snap b/tests-integration/fixtures/checking/248_do_empty_block/Main.snap new file mode 100644 index 00000000..b9efdf78 --- /dev/null +++ b/tests-integration/fixtures/checking/248_do_empty_block/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: ??? + +Types + +Diagnostics +error[EmptyDoBlock]: Empty do block + --> 6:8..6:10 + | +6 | test = do + | ^~ +error[CannotUnify]: Cannot unify '???' with 'Effect Int' + --> 5:1..5:19 + | +5 | test :: Effect Int + | ^~~~~~~~~~~~~~~~~~ +error[EmptyDoBlock]: Empty do block + --> 8:9..8:11 + | +8 | test' = do + | ^~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 8:1..8:11 + | +8 | test' = do + | ^~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/249_do_final_bind/Main.purs b/tests-integration/fixtures/checking/249_do_final_bind/Main.purs new file mode 100644 index 00000000..9c3019d6 --- /dev/null +++ b/tests-integration/fixtures/checking/249_do_final_bind/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind) +import Effect (Effect) + +test :: Effect Int +test = do + x <- pure 1 + +test' = do + x <- pure 1 diff --git a/tests-integration/fixtures/checking/249_do_final_bind/Main.snap b/tests-integration/fixtures/checking/249_do_final_bind/Main.snap new file mode 100644 index 00000000..3ec4ba80 --- /dev/null +++ b/tests-integration/fixtures/checking/249_do_final_bind/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: forall (t8 :: Type -> Type). Applicative (t8 :: Type -> Type) => (t8 :: Type -> Type) Int + +Types + +Diagnostics +warning[InvalidFinalBind]: Invalid final bind statement in do expression + --> 9:3..9:14 + | +9 | x <- pure 1 + | ^~~~~~~~~~~ +warning[InvalidFinalBind]: Invalid final bind statement in do expression + --> 12:3..12:14 + | +12 | x <- pure 1 + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/250_do_final_let/Main.purs b/tests-integration/fixtures/checking/250_do_final_let/Main.purs new file mode 100644 index 00000000..bfcd874e --- /dev/null +++ b/tests-integration/fixtures/checking/250_do_final_let/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Effect (Effect) + +test :: Effect Int +test = do + let x = 1 + +test' = do + let x = 1 diff --git a/tests-integration/fixtures/checking/250_do_final_let/Main.snap b/tests-integration/fixtures/checking/250_do_final_let/Main.snap new file mode 100644 index 00000000..e3b6b93c --- /dev/null +++ b/tests-integration/fixtures/checking/250_do_final_let/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: ??? + +Types + +Diagnostics +error[InvalidFinalLet]: Invalid final let statement in do expression + --> 7:3..7:12 + | +7 | let x = 1 + | ^~~~~~~~~ +error[CannotUnify]: Cannot unify '???' with 'Effect Int' + --> 5:1..5:19 + | +5 | test :: Effect Int + | ^~~~~~~~~~~~~~~~~~ +error[InvalidFinalLet]: Invalid final let statement in do expression + --> 10:3..10:12 + | +10 | let x = 1 + | ^~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 9:1..10:12 + | +9 | test' = do + | ^~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 15db4893..b9b727f7 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -509,3 +509,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_246_do_bind_only_main() { run_test("246_do_bind_only", "Main"); } #[rustfmt::skip] #[test] fn test_247_do_discard_not_in_scope_main() { run_test("247_do_discard_not_in_scope", "Main"); } + +#[rustfmt::skip] #[test] fn test_248_do_empty_block_main() { run_test("248_do_empty_block", "Main"); } + +#[rustfmt::skip] #[test] fn test_249_do_final_bind_main() { run_test("249_do_final_bind", "Main"); } + +#[rustfmt::skip] #[test] fn test_250_do_final_let_main() { run_test("250_do_final_let", "Main"); } From c9006550459e219a74e372e7a37d32014dbc247e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 05:38:52 +0800 Subject: [PATCH 051/386] Fix missing globalisation for Instance::kind_variables --- compiler-core/checking/src/algorithm/term_item.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 2ab4439b..df42d52a 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -192,6 +192,12 @@ where instance.constraints = constraints.collect(); + let kind_variables = instance.kind_variables.iter().map(|&k| { + transfer::globalize(state, context, k) + }); + + instance.kind_variables = kind_variables.collect(); + state.checked.instances.insert(instance_id, instance); // Capture implicit variables from the instance head before unbinding. From c90ee594304ada429fd6675760c7ab7f661d3056 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 05:49:28 +0800 Subject: [PATCH 052/386] Infer argument on invalid application An invalid kind for the function position in infer_surface_app_kind prevented implicit type variable arguments from being bound onto the environment. This was demonstrated by 251_lookup_implicit_panic which triggers a panic on the use of the type variable `a` on the instance declaration body. This commit adds inference for the `argument_t`, returning the elaborated type and an unknown kind for the application. --- compiler-core/checking/src/algorithm/kind.rs | 10 ++++- .../251_lookup_implicit_panic/Main.purs | 14 +++++++ .../251_lookup_implicit_panic/Main.snap | 39 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.purs create mode 100644 tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index 2af33955..a4cc85df 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -400,7 +400,15 @@ where infer_surface_app_kind(state, context, (function_t, function_k), argument) } - _ => Ok((context.prim.unknown, context.prim.unknown)), + _ => { + // TODO: Consider adding an error here for invalid type application. + // Even if the function type cannot be applied, the argument must + // still be inferred. For invalid applications on instance heads, + // this ensures that implicit variables are bound. + let (argument_t, _) = infer_surface_kind(state, context, argument)?; + let t = state.storage.intern(Type::Application(function_t, argument_t)); + Ok((t, context.prim.unknown)) + } } } diff --git a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.purs b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.purs new file mode 100644 index 00000000..947e64ab --- /dev/null +++ b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.purs @@ -0,0 +1,14 @@ +module Main where + +import Type.Proxy (Proxy(..)) + +class Identity a where + identity :: a -> a + +instance Identity (Undefined a) where + identity a = + let + proxy :: Proxy a + proxy = Proxy + in + a diff --git a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap new file mode 100644 index 00000000..628e2fcb --- /dev/null +++ b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap @@ -0,0 +1,39 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +identity :: forall (a :: Type). Identity (a :: Type) => (a :: Type) -> (a :: Type) + +Types +Identity :: Type -> Constraint + +Classes +class Identity (&0 :: Type) + +Instances +instance forall (&0 :: Type) (&1 :: ???). Identity (??? (&2 :: (&1 :: (&0 :: Type))) :: ???) + chain: 0 + +Diagnostics +error[NotInScope]: 'Undefined' is not in scope + --> 8:20..8:29 + | +8 | instance Identity (Undefined a) where + | ^~~~~~~~~ +error[CannotUnify]: Cannot unify '???' with 'Type' + --> 8:19..8:32 + | +8 | instance Identity (Undefined a) where + | ^~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 8:1..14:8 + | +8 | instance Identity (Undefined a) where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 8:1..14:8 + | +8 | instance Identity (Undefined a) where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index b9b727f7..61bfd6c8 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -515,3 +515,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_249_do_final_bind_main() { run_test("249_do_final_bind", "Main"); } #[rustfmt::skip] #[test] fn test_250_do_final_let_main() { run_test("250_do_final_let", "Main"); } + +#[rustfmt::skip] #[test] fn test_251_lookup_implicit_panic_main() { run_test("251_lookup_implicit_panic", "Main"); } From 02818ec81d7cb48226bfea003bd0e85d70a50170 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 06:04:53 +0800 Subject: [PATCH 053/386] Add InvalidTypeApplication error and test cases --- compiler-core/checking/src/algorithm/kind.rs | 13 +++++++-- compiler-core/checking/src/error.rs | 5 ++++ compiler-core/diagnostics/src/convert.rs | 13 +++++++++ .../Main.purs | 3 ++ .../Main.snap | 28 +++++++++++++++++++ .../Main.purs | 3 ++ .../Main.snap | 28 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 4 +++ 8 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.purs create mode 100644 tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.snap create mode 100644 tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.purs create mode 100644 tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index a4cc85df..b3825084 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -14,7 +14,7 @@ use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{substitute, transfer, unification}; use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, Variable}; -use crate::error::ErrorStep; +use crate::error::{ErrorKind, ErrorStep}; const MISSING_NAME: SmolStr = SmolStr::new_static(""); @@ -401,11 +401,20 @@ where } _ => { - // TODO: Consider adding an error here for invalid type application. // Even if the function type cannot be applied, the argument must // still be inferred. For invalid applications on instance heads, // this ensures that implicit variables are bound. let (argument_t, _) = infer_surface_kind(state, context, argument)?; + + let function_type = state.render_local_type(context, function_t); + let function_kind = state.render_local_type(context, function_k); + let argument_type = state.render_local_type(context, argument_t); + state.insert_error(ErrorKind::InvalidTypeApplication { + function_type, + function_kind, + argument_type, + }); + let t = state.storage.intern(Type::Application(function_t, argument_t)); Ok((t, context.prim.unknown)) } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 128fb3ce..2e653810 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -81,6 +81,11 @@ pub enum ErrorKind { expected: TypeErrorMessageId, actual: TypeErrorMessageId, }, + InvalidTypeApplication { + function_type: TypeErrorMessageId, + function_kind: TypeErrorMessageId, + argument_type: TypeErrorMessageId, + }, InvalidTypeOperator { kind_message: TypeErrorMessageId, }, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 5c22a48c..04c0bc48 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -252,6 +252,19 @@ impl ToDiagnostics for CheckError { format!("Instance member type mismatch: expected '{expected}', got '{actual}'"), ) } + ErrorKind::InvalidTypeApplication { function_type, function_kind, argument_type } => { + let function_type = lookup_message(*function_type); + let function_kind = lookup_message(*function_kind); + let argument_type = lookup_message(*argument_type); + ( + Severity::Error, + "InvalidTypeApplication", + format!( + "Cannot apply type '{function_type}' to '{argument_type}'. \ + '{function_type}' has kind '{function_kind}', which is not a function kind." + ), + ) + } ErrorKind::InvalidTypeOperator { kind_message } => { let msg = lookup_message(*kind_message); ( diff --git a/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.purs b/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.purs new file mode 100644 index 00000000..56548ca0 --- /dev/null +++ b/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.purs @@ -0,0 +1,3 @@ +module Main where + +type Bad = Int String diff --git a/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.snap b/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.snap new file mode 100644 index 00000000..cd9dfc5f --- /dev/null +++ b/tests-integration/fixtures/checking/252_invalid_type_application_basic/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Bad :: ??? + +Synonyms +Bad = Int String + Quantified = :0 + Kind = :0 + Type = :0 + + +Diagnostics +error[InvalidTypeApplication]: Cannot apply type 'Int' to 'String'. 'Int' has kind 'Type', which is not a function kind. + --> 3:12..3:22 + | +3 | type Bad = Int String + | ^~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 3:12..3:22 + | +3 | type Bad = Int String + | ^~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.purs b/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.purs new file mode 100644 index 00000000..5929643f --- /dev/null +++ b/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.purs @@ -0,0 +1,3 @@ +module Main where + +type Bad = Array Int String diff --git a/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.snap b/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.snap new file mode 100644 index 00000000..2e9312ac --- /dev/null +++ b/tests-integration/fixtures/checking/253_invalid_type_application_too_many/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Bad :: ??? + +Synonyms +Bad = Array Int String + Quantified = :0 + Kind = :0 + Type = :0 + + +Diagnostics +error[InvalidTypeApplication]: Cannot apply type 'Array Int' to 'String'. 'Array Int' has kind 'Type', which is not a function kind. + --> 3:12..3:28 + | +3 | type Bad = Array Int String + | ^~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Type' with '???' + --> 3:12..3:28 + | +3 | type Bad = Array Int String + | ^~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 61bfd6c8..36d2a0f7 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -517,3 +517,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_250_do_final_let_main() { run_test("250_do_final_let", "Main"); } #[rustfmt::skip] #[test] fn test_251_lookup_implicit_panic_main() { run_test("251_lookup_implicit_panic", "Main"); } + +#[rustfmt::skip] #[test] fn test_252_invalid_type_application_basic_main() { run_test("252_invalid_type_application_basic", "Main"); } + +#[rustfmt::skip] #[test] fn test_253_invalid_type_application_too_many_main() { run_test("253_invalid_type_application_too_many", "Main"); } From ca83523bfbf84c39ac91d61e02aa3285b49891e4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 28 Jan 2026 13:59:58 +0800 Subject: [PATCH 054/386] Update snapshots for InvalidTypeApplication --- .../fixtures/checking/251_lookup_implicit_panic/Main.snap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap index 628e2fcb..edaddd8f 100644 --- a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap +++ b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap @@ -22,6 +22,11 @@ error[NotInScope]: 'Undefined' is not in scope | 8 | instance Identity (Undefined a) where | ^~~~~~~~~ +error[InvalidTypeApplication]: Cannot apply type '???' to '(&0 :: ?2[:0])'. '???' has kind '???', which is not a function kind. + --> 8:20..8:31 + | +8 | instance Identity (Undefined a) where + | ^~~~~~~~~~~ error[CannotUnify]: Cannot unify '???' with 'Type' --> 8:19..8:32 | From 3b9a9d90086a31253187b04a4f65f545b32902fb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 00:31:40 +0800 Subject: [PATCH 055/386] Add compiler-compatibility CLI tool for fetching PureScript packages Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019c0568-ee9e-76f8-ac41-a66fa7e9c76f --- Cargo.lock | 799 ++++++++++++++++++++++++- Cargo.toml | 3 +- compiler-compatibility/Cargo.toml | 20 + compiler-compatibility/src/compat.rs | 5 + compiler-compatibility/src/error.rs | 42 ++ compiler-compatibility/src/layout.rs | 57 ++ compiler-compatibility/src/main.rs | 85 +++ compiler-compatibility/src/registry.rs | 142 +++++ compiler-compatibility/src/resolver.rs | 95 +++ compiler-compatibility/src/storage.rs | 52 ++ compiler-compatibility/src/types.rs | 103 ++++ compiler-compatibility/src/unpacker.rs | 34 ++ 12 files changed, 1434 insertions(+), 3 deletions(-) create mode 100644 compiler-compatibility/Cargo.toml create mode 100644 compiler-compatibility/src/compat.rs create mode 100644 compiler-compatibility/src/error.rs create mode 100644 compiler-compatibility/src/layout.rs create mode 100644 compiler-compatibility/src/main.rs create mode 100644 compiler-compatibility/src/registry.rs create mode 100644 compiler-compatibility/src/resolver.rs create mode 100644 compiler-compatibility/src/storage.rs create mode 100644 compiler-compatibility/src/types.rs create mode 100644 compiler-compatibility/src/unpacker.rs diff --git a/Cargo.lock b/Cargo.lock index b031ff53..e53ac54c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -140,12 +146,24 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -252,6 +270,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -362,6 +382,26 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compiler-compatibility" +version = "0.1.0" +dependencies = [ + "base64", + "clap", + "flate2", + "git2", + "hex", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2", + "tar", + "thiserror", + "tracing", + "tracing-subscriber", +] + [[package]] name = "compiler-scripts" version = "0.1.0" @@ -407,12 +447,46 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.6.0" @@ -569,6 +643,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.2.0" @@ -606,6 +689,17 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.6" @@ -618,6 +712,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -636,6 +740,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -732,6 +851,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -744,6 +874,21 @@ dependencies = [ "wasip2", ] +[[package]] +name = "git2" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +dependencies = [ + "bitflags 2.10.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glob" version = "0.3.3" @@ -763,6 +908,25 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" @@ -808,6 +972,131 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -958,6 +1247,22 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -997,6 +1302,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.83" @@ -1035,12 +1350,63 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "line-index" version = "0.1.2" @@ -1139,6 +1505,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minicov" version = "0.3.8" @@ -1149,6 +1521,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -1160,6 +1542,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -1212,6 +1611,50 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "papergrid" version = "0.17.0" @@ -1241,7 +1684,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1313,6 +1756,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -1483,6 +1932,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "regex" version = "1.12.2" @@ -1512,6 +1970,48 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "resolving" version = "0.1.0" @@ -1529,6 +2029,20 @@ dependencies = [ "smol_str", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rowan" version = "0.16.1" @@ -1566,12 +2080,51 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + [[package]] name = "same-file" version = "1.0.6" @@ -1581,12 +2134,54 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde" version = "1.0.228" @@ -1652,6 +2247,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1677,6 +2295,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" @@ -1765,6 +2389,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "sugar" version = "0.1.0" @@ -1788,6 +2418,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1810,6 +2449,27 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tabled" version = "0.20.0" @@ -1834,6 +2494,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -1841,7 +2512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1995,6 +2666,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -2015,6 +2706,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", "tower-layer", "tower-service", ] @@ -2105,6 +2819,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typed-arena" version = "2.0.2" @@ -2141,6 +2861,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -2171,6 +2897,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2197,6 +2929,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2327,6 +3068,44 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -2495,6 +3274,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.1" @@ -2559,6 +3348,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index f9eafa8c..1a090f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "compiler-bin", + "compiler-compatibility", "compiler-core/*", "compiler-lsp/*", "compiler-scripts", @@ -9,7 +10,7 @@ members = [ "tests-package-set", ] default-members = [ - "compiler-core/*", + "compiler-core/*", "compiler-lsp/*", ] resolver = "3" diff --git a/compiler-compatibility/Cargo.toml b/compiler-compatibility/Cargo.toml new file mode 100644 index 00000000..953cb508 --- /dev/null +++ b/compiler-compatibility/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "compiler-compatibility" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4", features = ["derive"] } +git2 = "0.20" +reqwest = { version = "0.12", features = ["blocking"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +semver = { version = "1", features = ["serde"] } +flate2 = "1" +tar = "0.4" +sha2 = "0.10" +hex = "0.4" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +base64 = "0.22.1" diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/src/compat.rs new file mode 100644 index 00000000..faad7411 --- /dev/null +++ b/compiler-compatibility/src/compat.rs @@ -0,0 +1,5 @@ +use std::path::Path; + +pub fn check_package(_package_dir: &Path) -> Vec { + todo!("Track D: QueryEngine integration scaffold") +} diff --git a/compiler-compatibility/src/error.rs b/compiler-compatibility/src/error.rs new file mode 100644 index 00000000..da5784c2 --- /dev/null +++ b/compiler-compatibility/src/error.rs @@ -0,0 +1,42 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CompatError { + #[error("io: {0}")] + Io(#[from] std::io::Error), + + #[error("json: {0}")] + Json(#[from] serde_json::Error), + + #[error("git: {0}")] + Git(#[from] git2::Error), + + #[error("http: {0}")] + Http(#[from] reqwest::Error), + + #[error("semver: {0}")] + Semver(#[from] semver::Error), + + #[error("package set missing package: {0}")] + MissingFromPackageSet(String), + + #[error("version mismatch for {name}: package-set {set_version} not in range {range}")] + VersionMismatch { name: String, set_version: String, range: String }, + + #[error("hash mismatch for {name}@{version}")] + HashMismatch { name: String, version: String }, + + #[error("manifest not found for {name}@{version}")] + ManifestNotFound { name: String, version: String }, + + #[error("no package sets found")] + NoPackageSets, + + #[error("package set not found: {0}")] + PackageSetNotFound(String), + + #[error("{0}")] + Other(String), +} + +pub type Result = std::result::Result; diff --git a/compiler-compatibility/src/layout.rs b/compiler-compatibility/src/layout.rs new file mode 100644 index 00000000..e6f0138f --- /dev/null +++ b/compiler-compatibility/src/layout.rs @@ -0,0 +1,57 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct Layout { + pub root: PathBuf, + pub repos_dir: PathBuf, + pub registry_dir: PathBuf, + pub index_dir: PathBuf, + pub cache_tarballs_dir: PathBuf, + pub packages_dir: PathBuf, +} + +impl Layout { + pub fn new(root: impl AsRef) -> Self { + let root = root.as_ref().to_path_buf(); + let repos_dir = root.join("repos"); + let registry_dir = repos_dir.join("registry"); + let index_dir = repos_dir.join("registry-index"); + let cache_tarballs_dir = root.join("cache").join("tarballs"); + let packages_dir = root.join("packages"); + + Self { root, repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } + } + + pub fn package_sets_dir(&self) -> PathBuf { + self.registry_dir.join("package-sets") + } + + pub fn metadata_dir(&self) -> PathBuf { + self.registry_dir.join("metadata") + } + + pub fn index_path(&self, name: &str) -> PathBuf { + let chars: Vec = name.chars().collect(); + match chars.len() { + 1 => self.index_dir.join("1").join(name), + 2 => self.index_dir.join("2").join(name), + 3 => { + let first_char: String = chars[..1].iter().collect(); + self.index_dir.join("3").join(first_char).join(name) + } + _ => { + let first_two: String = chars[..2].iter().collect(); + let second_two: String = chars[2..4].iter().collect(); + self.index_dir.join(first_two).join(second_two).join(name) + } + } + } + + pub fn tarball_cache_path(&self, name: &str, version: &str) -> PathBuf { + self.cache_tarballs_dir.join(format!("{}-{}.tar.gz", name, version)) + } + + pub fn package_dir(&self, name: &str, version: &str) -> PathBuf { + self.packages_dir.join(format!("{}-{}", name, version)) + } +} diff --git a/compiler-compatibility/src/main.rs b/compiler-compatibility/src/main.rs new file mode 100644 index 00000000..dc23962b --- /dev/null +++ b/compiler-compatibility/src/main.rs @@ -0,0 +1,85 @@ +mod compat; +mod error; +mod layout; +mod registry; +mod resolver; +mod storage; +mod types; +mod unpacker; + +use clap::Parser; +use registry::RegistryReader; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +#[command(name = "compiler-compatibility")] +#[command(about = "Fetch PureScript packages for compatibility testing")] +struct Cli { + #[arg(help = "Package names to fetch and unpack")] + packages: Vec, + + #[arg(long, help = "Use specific package set version (default: latest)")] + package_set: Option, + + #[arg(long, help = "List available package set versions")] + list_sets: bool, + + #[arg(long, help = "Update local registry repos (git pull)")] + update: bool, + + #[arg(long, default_value = "target/compiler-compatibility", help = "Output directory")] + output: PathBuf, + + #[arg(long, help = "Disable tarball caching")] + no_cache: bool, + + #[arg(short, long, help = "Verbose output")] + verbose: bool, +} + +fn main() -> error::Result<()> { + let cli = Cli::parse(); + + let filter = if cli.verbose { "debug" } else { "info" }; + tracing_subscriber::fmt().with_env_filter(filter).init(); + + let layout = layout::Layout::new(&cli.output); + let registry = registry::Registry::new(layout.clone()); + + registry.ensure_repos(cli.update)?; + + if cli.list_sets { + let sets = registry.list_package_sets()?; + for set in sets { + println!("{}", set); + } + return Ok(()); + } + + if cli.packages.is_empty() { + println!("No packages specified. Use --help for usage."); + return Ok(()); + } + + let package_set = registry.read_package_set(cli.package_set.as_deref())?; + tracing::info!(version = %package_set.version, "Using package set"); + + let resolved = resolver::resolve(&cli.packages, &package_set, ®istry)?; + tracing::info!(count = resolved.packages.len(), "Resolved packages"); + + for (name, version) in &resolved.packages { + let metadata = registry.read_metadata(name)?; + let published = metadata.published.get(version).ok_or_else(|| { + error::CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } + })?; + + let tarball = storage::fetch_tarball(name, version, &layout, cli.no_cache)?; + storage::verify_tarball(&tarball, &published.hash, name, version)?; + unpacker::unpack_tarball(&tarball, &layout.package_dir(name, version))?; + + tracing::info!(name, version, "Unpacked"); + } + + tracing::info!(dir = %layout.packages_dir.display(), "Done"); + Ok(()) +} diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs new file mode 100644 index 00000000..f11d6dbb --- /dev/null +++ b/compiler-compatibility/src/registry.rs @@ -0,0 +1,142 @@ +use std::{fs, io::BufRead}; + +use git2::{build::RepoBuilder, FetchOptions, Repository}; + +use crate::{ + error::{CompatError, Result}, + layout::Layout, + types::{Manifest, Metadata, PackageSet}, +}; + +const REGISTRY_URL: &str = "https://github.com/purescript/registry"; +const REGISTRY_INDEX_URL: &str = "https://github.com/purescript/registry-index"; + +pub trait RegistryReader { + fn list_package_sets(&self) -> Result>; + fn read_package_set(&self, version: Option<&str>) -> Result; + fn read_manifest_versions(&self, name: &str) -> Result>; + fn read_metadata(&self, name: &str) -> Result; +} + +pub struct Registry { + layout: Layout, +} + +impl Registry { + pub fn new(layout: Layout) -> Self { + Self { layout } + } + + pub fn ensure_repos(&self, update: bool) -> Result<()> { + fs::create_dir_all(&self.layout.repos_dir)?; + + self.ensure_repo(REGISTRY_URL, &self.layout.registry_dir, update)?; + self.ensure_repo(REGISTRY_INDEX_URL, &self.layout.index_dir, update)?; + + Ok(()) + } + + fn ensure_repo(&self, url: &str, path: &std::path::Path, update: bool) -> Result<()> { + if path.exists() { + if update { + let repo = Repository::open(path)?; + let mut remote = repo.find_remote("origin")?; + remote.fetch(&["master"], None, None)?; + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + let commit = repo.reference_to_annotated_commit(&fetch_head)?; + let (analysis, _) = repo.merge_analysis(&[&commit])?; + + if analysis.is_fast_forward() || analysis.is_normal() { + let refname = "refs/heads/master"; + let mut reference = repo.find_reference(refname)?; + reference.set_target(commit.id(), "pull: fast-forward")?; + repo.set_head(refname)?; + repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; + } + } + } else { + let mut fetch_opts = FetchOptions::new(); + fetch_opts.depth(1); + + RepoBuilder::new().fetch_options(fetch_opts).clone(url, path)?; + } + + Ok(()) + } + + pub fn layout(&self) -> &Layout { + &self.layout + } +} + +impl RegistryReader for Registry { + fn list_package_sets(&self) -> Result> { + let package_sets_dir = self.layout.package_sets_dir(); + let mut versions: Vec = Vec::new(); + + for entry in fs::read_dir(&package_sets_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().is_some_and(|ext| ext == "json") { + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + if let Ok(version) = stem.parse::() { + versions.push(version); + } + } + } + } + + if versions.is_empty() { + return Err(CompatError::NoPackageSets); + } + + versions.sort_by(|a, b| b.cmp(a)); + + Ok(versions.into_iter().map(|v| v.to_string()).collect()) + } + + fn read_package_set(&self, version: Option<&str>) -> Result { + let version = match version { + Some(v) => v.to_string(), + None => self.list_package_sets()?.into_iter().next().ok_or(CompatError::NoPackageSets)?, + }; + + let path = self.layout.package_sets_dir().join(format!("{}.json", version)); + + if !path.exists() { + return Err(CompatError::PackageSetNotFound(version)); + } + + let content = fs::read_to_string(&path)?; + let package_set: PackageSet = serde_json::from_str(&content)?; + + Ok(package_set) + } + + fn read_manifest_versions(&self, name: &str) -> Result> { + let path = self.layout.index_path(name); + let file = fs::File::open(&path)?; + let reader = std::io::BufReader::new(file); + + let mut manifests = Vec::new(); + for line in reader.lines() { + let line = line?; + if !line.is_empty() { + let manifest: Manifest = serde_json::from_str(&line)?; + manifests.push(manifest); + } + } + + Ok(manifests) + } + + fn read_metadata(&self, name: &str) -> Result { + let path = self.layout.metadata_dir().join(format!("{}.json", name)); + let content = fs::read_to_string(&path)?; + let metadata: Metadata = serde_json::from_str(&content)?; + + Ok(metadata) + } +} diff --git a/compiler-compatibility/src/resolver.rs b/compiler-compatibility/src/resolver.rs new file mode 100644 index 00000000..c9265acb --- /dev/null +++ b/compiler-compatibility/src/resolver.rs @@ -0,0 +1,95 @@ +use std::collections::{BTreeMap, HashSet}; + +use semver::Version; + +use crate::{ + error::{CompatError, Result}, + registry::RegistryReader, + types::{PackageSet, ResolvedSet}, +}; + +pub fn resolve( + root_packages: &[String], + package_set: &PackageSet, + registry: &impl RegistryReader, +) -> Result { + let mut resolved = BTreeMap::new(); + let mut visited = HashSet::new(); + + for name in root_packages { + let version = package_set + .packages + .packages + .get(name) + .ok_or_else(|| CompatError::MissingFromPackageSet(name.clone()))?; + resolve_recursive(name, version, package_set, registry, &mut resolved, &mut visited)?; + } + + Ok(ResolvedSet { packages: resolved }) +} + +fn resolve_recursive( + name: &str, + version: &str, + package_set: &PackageSet, + registry: &impl RegistryReader, + resolved: &mut BTreeMap, + visited: &mut HashSet, +) -> Result<()> { + if !visited.insert(name.to_string()) { + return Ok(()); + } + + resolved.insert(name.to_string(), version.to_string()); + + let manifests = registry.read_manifest_versions(name)?; + let parsed_version: Version = version.parse()?; + let manifest = manifests + .iter() + .find(|m| m.version == parsed_version) + .ok_or_else(|| CompatError::ManifestNotFound { + name: name.to_string(), + version: version.to_string(), + })?; + + for (dep_name, range) in &manifest.dependencies { + let dep_version = package_set + .packages + .packages + .get(dep_name) + .ok_or_else(|| CompatError::MissingFromPackageSet(dep_name.clone()))?; + + if !version_satisfies_range(dep_version, range)? { + return Err(CompatError::VersionMismatch { + name: dep_name.clone(), + set_version: dep_version.clone(), + range: range.clone(), + }); + } + + resolve_recursive(dep_name, dep_version, package_set, registry, resolved, visited)?; + } + + Ok(()) +} + +fn version_satisfies_range(version: &str, range: &str) -> Result { + let parsed_version: Version = version.parse()?; + let parts: Vec<&str> = range.split_whitespace().collect(); + + if parts.len() != 2 { + return Err(CompatError::Other(format!("invalid version range format: {}", range))); + } + + let lower = parts[0] + .strip_prefix(">=") + .ok_or_else(|| CompatError::Other(format!("expected >= prefix in range: {}", range)))?; + let upper = parts[1] + .strip_prefix('<') + .ok_or_else(|| CompatError::Other(format!("expected < prefix in range: {}", range)))?; + + let lower_version: Version = lower.parse()?; + let upper_version: Version = upper.parse()?; + + Ok(parsed_version >= lower_version && parsed_version < upper_version) +} diff --git a/compiler-compatibility/src/storage.rs b/compiler-compatibility/src/storage.rs new file mode 100644 index 00000000..a210f5ce --- /dev/null +++ b/compiler-compatibility/src/storage.rs @@ -0,0 +1,52 @@ +use std::{fs, io::Read, path::PathBuf}; + +use sha2::{Digest, Sha256}; + +use crate::{ + error::{CompatError, Result}, + layout::Layout, +}; + +pub fn tarball_url(name: &str, version: &str) -> String { + format!("https://packages.registry.purescript.org/{}/{}.tar.gz", name, version) +} + +pub fn fetch_tarball(name: &str, version: &str, layout: &Layout, no_cache: bool) -> Result { + let tarball_path = layout.tarball_cache_path(name, version); + + if !no_cache && tarball_path.exists() { + return Ok(tarball_path); + } + + fs::create_dir_all(&layout.cache_tarballs_dir)?; + + let url = tarball_url(name, version); + let response = reqwest::blocking::get(&url)?; + let bytes = response.bytes()?; + + let part_path = tarball_path.with_extension("tar.gz.part"); + fs::write(&part_path, &bytes)?; + fs::rename(&part_path, &tarball_path)?; + + Ok(tarball_path) +} + +pub fn verify_tarball(path: &PathBuf, expected_sha256: &str, name: &str, version: &str) -> Result<()> { + use base64::{engine::general_purpose::STANDARD, Engine}; + + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + hasher.update(&buffer); + let hash = hasher.finalize(); + let hash_b64 = STANDARD.encode(hash); + + let expected_b64 = expected_sha256.strip_prefix("sha256-").unwrap_or(expected_sha256); + + if hash_b64 != expected_b64 { + return Err(CompatError::HashMismatch { name: name.to_string(), version: version.to_string() }); + } + + Ok(()) +} diff --git a/compiler-compatibility/src/types.rs b/compiler-compatibility/src/types.rs new file mode 100644 index 00000000..547ae2dc --- /dev/null +++ b/compiler-compatibility/src/types.rs @@ -0,0 +1,103 @@ +use serde::Deserialize; +use std::{collections::HashMap, path::PathBuf}; + +#[derive(Debug, Clone, Deserialize)] +pub struct PackageSet { + pub version: String, + pub compiler: String, + pub published: String, + #[serde(flatten)] + pub packages: PackageSetPackages, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackageSetPackages { + pub packages: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Manifest { + pub name: String, + pub version: semver::Version, + pub license: String, + #[serde(default)] + pub description: Option, + pub location: Location, + #[serde(default)] + pub dependencies: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum Location { + GitHub { + #[serde(rename = "githubOwner")] + owner: String, + #[serde(rename = "githubRepo")] + repo: String, + subdir: Option, + }, + Git { + #[serde(rename = "gitUrl")] + url: String, + subdir: Option, + }, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Metadata { + pub location: Location, + pub owners: Option>, + pub published: HashMap, + #[serde(default)] + pub unpublished: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PublishedVersion { + pub hash: String, + pub ref_: Option, + pub bytes: Option, + #[serde(rename = "publishedTime")] + pub published_time: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct UnpublishedVersion { + pub ref_: Option, + pub reason: String, + #[serde(rename = "publishedTime")] + pub published_time: String, + #[serde(rename = "unpublishedTime")] + pub unpublished_time: String, +} + +#[derive(Debug, Clone)] +pub struct ResolvedPackage { + pub name: String, + pub version: String, +} + +#[derive(Debug, Clone, Default)] +pub struct ResolvedSet { + pub packages: std::collections::BTreeMap, +} + +#[derive(Debug, Clone)] +pub struct CompatConfig { + pub root: PathBuf, + pub no_cache: bool, + pub update_repos: bool, + pub package_set: Option, +} + +impl Default for CompatConfig { + fn default() -> Self { + Self { + root: PathBuf::from("target/compiler-compatibility"), + no_cache: false, + update_repos: false, + package_set: None, + } + } +} diff --git a/compiler-compatibility/src/unpacker.rs b/compiler-compatibility/src/unpacker.rs new file mode 100644 index 00000000..66e9d6db --- /dev/null +++ b/compiler-compatibility/src/unpacker.rs @@ -0,0 +1,34 @@ +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, +}; + +use flate2::read::GzDecoder; +use tar::Archive; + +use crate::error::{CompatError, Result}; + +pub fn unpack_tarball(tarball: &Path, dest_dir: &Path) -> Result { + fs::create_dir_all(dest_dir)?; + + let file = File::open(tarball)?; + let decoder = GzDecoder::new(file); + let mut archive = Archive::new(decoder); + + for entry in archive.entries()? { + let mut entry = entry?; + let path = entry.path()?; + + if path.components().any(|c| c == std::path::Component::ParentDir) { + return Err(CompatError::Other(format!("path traversal detected: {}", path.display()))); + } + + if path.is_absolute() { + return Err(CompatError::Other(format!("absolute path in archive: {}", path.display()))); + } + + entry.unpack_in(dest_dir)?; + } + + Ok(dest_dir.to_path_buf()) +} From 2e90c0cd6176967242def77796f16920ae388234 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 00:38:40 +0800 Subject: [PATCH 056/386] Extract purescript-registry crate from compiler-compatibility Amp-Thread-ID: https://ampcode.com/threads/T-019c0573-57d6-74c4-8e9d-bb13468493af Co-authored-by: Amp --- Cargo.lock | 13 +++- Cargo.toml | 1 + compiler-compatibility/Cargo.toml | 5 +- compiler-compatibility/src/error.rs | 12 +--- compiler-compatibility/src/layout.rs | 34 +++------ compiler-compatibility/src/main.rs | 13 ++-- compiler-compatibility/src/registry.rs | 93 +++--------------------- compiler-compatibility/src/resolver.rs | 4 +- compiler-compatibility/src/types.rs | 74 +------------------ purescript-registry/Cargo.toml | 10 +++ purescript-registry/src/error.rs | 21 ++++++ purescript-registry/src/layout.rs | 49 +++++++++++++ purescript-registry/src/lib.rs | 9 +++ purescript-registry/src/reader.rs | 99 ++++++++++++++++++++++++++ purescript-registry/src/types.rs | 79 ++++++++++++++++++++ 15 files changed, 312 insertions(+), 204 deletions(-) create mode 100644 purescript-registry/Cargo.toml create mode 100644 purescript-registry/src/error.rs create mode 100644 purescript-registry/src/layout.rs create mode 100644 purescript-registry/src/lib.rs create mode 100644 purescript-registry/src/reader.rs create mode 100644 purescript-registry/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index e53ac54c..b9c48e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,10 +391,9 @@ dependencies = [ "flate2", "git2", "hex", + "purescript-registry", "reqwest", "semver", - "serde", - "serde_json", "sha2", "tar", "thiserror", @@ -1878,6 +1877,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "purescript-registry" +version = "0.1.0" +dependencies = [ + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "quote" version = "1.0.42" diff --git a/Cargo.toml b/Cargo.toml index 1a090f1e..def1a3f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "compiler-lsp/*", "compiler-scripts", "docs/src/wasm", + "purescript-registry", "tests-integration", "tests-package-set", ] diff --git a/compiler-compatibility/Cargo.toml b/compiler-compatibility/Cargo.toml index 953cb508..53ff1baa 100644 --- a/compiler-compatibility/Cargo.toml +++ b/compiler-compatibility/Cargo.toml @@ -4,12 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] +purescript-registry = { path = "../purescript-registry" } clap = { version = "4", features = ["derive"] } git2 = "0.20" reqwest = { version = "0.12", features = ["blocking"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -semver = { version = "1", features = ["serde"] } +semver = "1" flate2 = "1" tar = "0.4" sha2 = "0.10" diff --git a/compiler-compatibility/src/error.rs b/compiler-compatibility/src/error.rs index da5784c2..102e9f08 100644 --- a/compiler-compatibility/src/error.rs +++ b/compiler-compatibility/src/error.rs @@ -5,9 +5,6 @@ pub enum CompatError { #[error("io: {0}")] Io(#[from] std::io::Error), - #[error("json: {0}")] - Json(#[from] serde_json::Error), - #[error("git: {0}")] Git(#[from] git2::Error), @@ -17,6 +14,9 @@ pub enum CompatError { #[error("semver: {0}")] Semver(#[from] semver::Error), + #[error("registry: {0}")] + Registry(#[from] purescript_registry::RegistryError), + #[error("package set missing package: {0}")] MissingFromPackageSet(String), @@ -29,12 +29,6 @@ pub enum CompatError { #[error("manifest not found for {name}@{version}")] ManifestNotFound { name: String, version: String }, - #[error("no package sets found")] - NoPackageSets, - - #[error("package set not found: {0}")] - PackageSetNotFound(String), - #[error("{0}")] Other(String), } diff --git a/compiler-compatibility/src/layout.rs b/compiler-compatibility/src/layout.rs index e6f0138f..0807f286 100644 --- a/compiler-compatibility/src/layout.rs +++ b/compiler-compatibility/src/layout.rs @@ -1,5 +1,10 @@ use std::path::{Path, PathBuf}; +use purescript_registry::RegistryLayout; + +/// Layout for compiler-compatibility tool directories. +/// +/// Includes paths for repository checkouts, tarball cache, and unpacked packages. #[derive(Debug, Clone)] pub struct Layout { pub root: PathBuf, @@ -11,7 +16,7 @@ pub struct Layout { } impl Layout { - pub fn new(root: impl AsRef) -> Self { + pub fn new(root: impl AsRef) -> Layout { let root = root.as_ref().to_path_buf(); let repos_dir = root.join("repos"); let registry_dir = repos_dir.join("registry"); @@ -19,32 +24,11 @@ impl Layout { let cache_tarballs_dir = root.join("cache").join("tarballs"); let packages_dir = root.join("packages"); - Self { root, repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } - } - - pub fn package_sets_dir(&self) -> PathBuf { - self.registry_dir.join("package-sets") - } - - pub fn metadata_dir(&self) -> PathBuf { - self.registry_dir.join("metadata") + Layout { root, repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } } - pub fn index_path(&self, name: &str) -> PathBuf { - let chars: Vec = name.chars().collect(); - match chars.len() { - 1 => self.index_dir.join("1").join(name), - 2 => self.index_dir.join("2").join(name), - 3 => { - let first_char: String = chars[..1].iter().collect(); - self.index_dir.join("3").join(first_char).join(name) - } - _ => { - let first_two: String = chars[..2].iter().collect(); - let second_two: String = chars[2..4].iter().collect(); - self.index_dir.join(first_two).join(second_two).join(name) - } - } + pub fn registry_layout(&self) -> RegistryLayout { + RegistryLayout::new(&self.registry_dir, &self.index_dir) } pub fn tarball_cache_path(&self, name: &str, version: &str) -> PathBuf { diff --git a/compiler-compatibility/src/main.rs b/compiler-compatibility/src/main.rs index dc23962b..6ddb10b0 100644 --- a/compiler-compatibility/src/main.rs +++ b/compiler-compatibility/src/main.rs @@ -7,10 +7,11 @@ mod storage; mod types; mod unpacker; -use clap::Parser; -use registry::RegistryReader; use std::path::PathBuf; +use clap::Parser; +use purescript_registry::RegistryReader; + #[derive(Parser, Debug)] #[command(name = "compiler-compatibility")] #[command(about = "Fetch PureScript packages for compatibility testing")] @@ -49,7 +50,7 @@ fn main() -> error::Result<()> { registry.ensure_repos(cli.update)?; if cli.list_sets { - let sets = registry.list_package_sets()?; + let sets = registry.reader().list_package_sets()?; for set in sets { println!("{}", set); } @@ -61,14 +62,14 @@ fn main() -> error::Result<()> { return Ok(()); } - let package_set = registry.read_package_set(cli.package_set.as_deref())?; + let package_set = registry.reader().read_package_set(cli.package_set.as_deref())?; tracing::info!(version = %package_set.version, "Using package set"); - let resolved = resolver::resolve(&cli.packages, &package_set, ®istry)?; + let resolved = resolver::resolve(&cli.packages, &package_set, registry.reader())?; tracing::info!(count = resolved.packages.len(), "Resolved packages"); for (name, version) in &resolved.packages { - let metadata = registry.read_metadata(name)?; + let metadata = registry.reader().read_metadata(name)?; let published = metadata.published.get(version).ok_or_else(|| { error::CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } })?; diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs index f11d6dbb..abb3b8f7 100644 --- a/compiler-compatibility/src/registry.rs +++ b/compiler-compatibility/src/registry.rs @@ -1,30 +1,22 @@ -use std::{fs, io::BufRead}; +use std::fs; use git2::{build::RepoBuilder, FetchOptions, Repository}; +use purescript_registry::FsRegistry; -use crate::{ - error::{CompatError, Result}, - layout::Layout, - types::{Manifest, Metadata, PackageSet}, -}; +use crate::{error::Result, layout::Layout}; const REGISTRY_URL: &str = "https://github.com/purescript/registry"; const REGISTRY_INDEX_URL: &str = "https://github.com/purescript/registry-index"; -pub trait RegistryReader { - fn list_package_sets(&self) -> Result>; - fn read_package_set(&self, version: Option<&str>) -> Result; - fn read_manifest_versions(&self, name: &str) -> Result>; - fn read_metadata(&self, name: &str) -> Result; -} - pub struct Registry { layout: Layout, + reader: FsRegistry, } impl Registry { - pub fn new(layout: Layout) -> Self { - Self { layout } + pub fn new(layout: Layout) -> Registry { + let reader = FsRegistry::new(layout.registry_layout()); + Registry { layout, reader } } pub fn ensure_repos(&self, update: bool) -> Result<()> { @@ -68,75 +60,8 @@ impl Registry { pub fn layout(&self) -> &Layout { &self.layout } -} - -impl RegistryReader for Registry { - fn list_package_sets(&self) -> Result> { - let package_sets_dir = self.layout.package_sets_dir(); - let mut versions: Vec = Vec::new(); - - for entry in fs::read_dir(&package_sets_dir)? { - let entry = entry?; - let path = entry.path(); - - if path.extension().is_some_and(|ext| ext == "json") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - if let Ok(version) = stem.parse::() { - versions.push(version); - } - } - } - } - - if versions.is_empty() { - return Err(CompatError::NoPackageSets); - } - - versions.sort_by(|a, b| b.cmp(a)); - - Ok(versions.into_iter().map(|v| v.to_string()).collect()) - } - - fn read_package_set(&self, version: Option<&str>) -> Result { - let version = match version { - Some(v) => v.to_string(), - None => self.list_package_sets()?.into_iter().next().ok_or(CompatError::NoPackageSets)?, - }; - - let path = self.layout.package_sets_dir().join(format!("{}.json", version)); - - if !path.exists() { - return Err(CompatError::PackageSetNotFound(version)); - } - - let content = fs::read_to_string(&path)?; - let package_set: PackageSet = serde_json::from_str(&content)?; - - Ok(package_set) - } - - fn read_manifest_versions(&self, name: &str) -> Result> { - let path = self.layout.index_path(name); - let file = fs::File::open(&path)?; - let reader = std::io::BufReader::new(file); - - let mut manifests = Vec::new(); - for line in reader.lines() { - let line = line?; - if !line.is_empty() { - let manifest: Manifest = serde_json::from_str(&line)?; - manifests.push(manifest); - } - } - - Ok(manifests) - } - - fn read_metadata(&self, name: &str) -> Result { - let path = self.layout.metadata_dir().join(format!("{}.json", name)); - let content = fs::read_to_string(&path)?; - let metadata: Metadata = serde_json::from_str(&content)?; - Ok(metadata) + pub fn reader(&self) -> &FsRegistry { + &self.reader } } diff --git a/compiler-compatibility/src/resolver.rs b/compiler-compatibility/src/resolver.rs index c9265acb..7fe9dec7 100644 --- a/compiler-compatibility/src/resolver.rs +++ b/compiler-compatibility/src/resolver.rs @@ -1,11 +1,11 @@ use std::collections::{BTreeMap, HashSet}; +use purescript_registry::{PackageSet, RegistryReader}; use semver::Version; use crate::{ error::{CompatError, Result}, - registry::RegistryReader, - types::{PackageSet, ResolvedSet}, + types::ResolvedSet, }; pub fn resolve( diff --git a/compiler-compatibility/src/types.rs b/compiler-compatibility/src/types.rs index 547ae2dc..4b4aea4d 100644 --- a/compiler-compatibility/src/types.rs +++ b/compiler-compatibility/src/types.rs @@ -1,76 +1,4 @@ -use serde::Deserialize; -use std::{collections::HashMap, path::PathBuf}; - -#[derive(Debug, Clone, Deserialize)] -pub struct PackageSet { - pub version: String, - pub compiler: String, - pub published: String, - #[serde(flatten)] - pub packages: PackageSetPackages, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct PackageSetPackages { - pub packages: HashMap, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Manifest { - pub name: String, - pub version: semver::Version, - pub license: String, - #[serde(default)] - pub description: Option, - pub location: Location, - #[serde(default)] - pub dependencies: HashMap, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -pub enum Location { - GitHub { - #[serde(rename = "githubOwner")] - owner: String, - #[serde(rename = "githubRepo")] - repo: String, - subdir: Option, - }, - Git { - #[serde(rename = "gitUrl")] - url: String, - subdir: Option, - }, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Metadata { - pub location: Location, - pub owners: Option>, - pub published: HashMap, - #[serde(default)] - pub unpublished: HashMap, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct PublishedVersion { - pub hash: String, - pub ref_: Option, - pub bytes: Option, - #[serde(rename = "publishedTime")] - pub published_time: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct UnpublishedVersion { - pub ref_: Option, - pub reason: String, - #[serde(rename = "publishedTime")] - pub published_time: String, - #[serde(rename = "unpublishedTime")] - pub unpublished_time: String, -} +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct ResolvedPackage { diff --git a/purescript-registry/Cargo.toml b/purescript-registry/Cargo.toml new file mode 100644 index 00000000..fd0a4e70 --- /dev/null +++ b/purescript-registry/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "purescript-registry" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +semver = { version = "1", features = ["serde"] } +thiserror = "2" diff --git a/purescript-registry/src/error.rs b/purescript-registry/src/error.rs new file mode 100644 index 00000000..b9c5bc8d --- /dev/null +++ b/purescript-registry/src/error.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RegistryError { + #[error("io: {0}")] + Io(#[from] std::io::Error), + + #[error("json: {0}")] + Json(#[from] serde_json::Error), + + #[error("semver: {0}")] + Semver(#[from] semver::Error), + + #[error("no package sets found")] + NoPackageSets, + + #[error("package set not found: {0}")] + PackageSetNotFound(String), +} + +pub type Result = std::result::Result; diff --git a/purescript-registry/src/layout.rs b/purescript-registry/src/layout.rs new file mode 100644 index 00000000..2b3cacd7 --- /dev/null +++ b/purescript-registry/src/layout.rs @@ -0,0 +1,49 @@ +use std::path::{Path, PathBuf}; + +/// Layout for PureScript registry and registry-index directories. +/// +/// This represents the on-disk structure of the registry repositories, +/// independent of where they are located or how they are managed. +#[derive(Debug, Clone)] +pub struct RegistryLayout { + pub registry_dir: PathBuf, + pub index_dir: PathBuf, +} + +impl RegistryLayout { + pub fn new(registry_dir: impl AsRef, index_dir: impl AsRef) -> RegistryLayout { + RegistryLayout { registry_dir: registry_dir.as_ref().to_path_buf(), index_dir: index_dir.as_ref().to_path_buf() } + } + + pub fn package_sets_dir(&self) -> PathBuf { + self.registry_dir.join("package-sets") + } + + pub fn metadata_dir(&self) -> PathBuf { + self.registry_dir.join("metadata") + } + + /// Returns the path to a package's manifest in the registry index. + /// + /// The index uses a sharding scheme based on package name length: + /// - 1-char names: `1/{name}` + /// - 2-char names: `2/{name}` + /// - 3-char names: `3/{first-char}/{name}` + /// - 4+ char names: `{first-two}/{chars-3-4}/{name}` + pub fn index_path(&self, name: &str) -> PathBuf { + let chars: Vec = name.chars().collect(); + match chars.len() { + 1 => self.index_dir.join("1").join(name), + 2 => self.index_dir.join("2").join(name), + 3 => { + let first_char: String = chars[..1].iter().collect(); + self.index_dir.join("3").join(first_char).join(name) + } + _ => { + let first_two: String = chars[..2].iter().collect(); + let second_two: String = chars[2..4].iter().collect(); + self.index_dir.join(first_two).join(second_two).join(name) + } + } + } +} diff --git a/purescript-registry/src/lib.rs b/purescript-registry/src/lib.rs new file mode 100644 index 00000000..a9fbc9d9 --- /dev/null +++ b/purescript-registry/src/lib.rs @@ -0,0 +1,9 @@ +mod error; +mod layout; +mod reader; +mod types; + +pub use error::{RegistryError, Result}; +pub use layout::RegistryLayout; +pub use reader::{FsRegistry, RegistryReader}; +pub use types::{Location, Manifest, Metadata, PackageSet, PackageSetPackages, PublishedVersion, UnpublishedVersion}; diff --git a/purescript-registry/src/reader.rs b/purescript-registry/src/reader.rs new file mode 100644 index 00000000..d5e939c2 --- /dev/null +++ b/purescript-registry/src/reader.rs @@ -0,0 +1,99 @@ +use std::{fs, io::BufRead}; + +use crate::{ + error::{RegistryError, Result}, + layout::RegistryLayout, + types::{Manifest, Metadata, PackageSet}, +}; + +pub trait RegistryReader { + fn list_package_sets(&self) -> Result>; + fn read_package_set(&self, version: Option<&str>) -> Result; + fn read_manifest_versions(&self, name: &str) -> Result>; + fn read_metadata(&self, name: &str) -> Result; +} + +pub struct FsRegistry { + layout: RegistryLayout, +} + +impl FsRegistry { + pub fn new(layout: RegistryLayout) -> FsRegistry { + FsRegistry { layout } + } + + pub fn layout(&self) -> &RegistryLayout { + &self.layout + } +} + +impl RegistryReader for FsRegistry { + fn list_package_sets(&self) -> Result> { + let package_sets_dir = self.layout.package_sets_dir(); + let mut versions: Vec = Vec::new(); + + for entry in fs::read_dir(&package_sets_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().is_some_and(|ext| ext == "json") { + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + if let Ok(version) = stem.parse::() { + versions.push(version); + } + } + } + } + + if versions.is_empty() { + return Err(RegistryError::NoPackageSets); + } + + versions.sort_by(|a, b| b.cmp(a)); + + Ok(versions.into_iter().map(|v| v.to_string()).collect()) + } + + fn read_package_set(&self, version: Option<&str>) -> Result { + let version = match version { + Some(v) => v.to_string(), + None => self.list_package_sets()?.into_iter().next().ok_or(RegistryError::NoPackageSets)?, + }; + + let path = self.layout.package_sets_dir().join(format!("{}.json", version)); + + if !path.exists() { + return Err(RegistryError::PackageSetNotFound(version)); + } + + let content = fs::read_to_string(&path)?; + let package_set: PackageSet = serde_json::from_str(&content)?; + + Ok(package_set) + } + + fn read_manifest_versions(&self, name: &str) -> Result> { + let path = self.layout.index_path(name); + let file = fs::File::open(&path)?; + let reader = std::io::BufReader::new(file); + + let mut manifests = Vec::new(); + for line in reader.lines() { + let line = line?; + if !line.is_empty() { + let manifest: Manifest = serde_json::from_str(&line)?; + manifests.push(manifest); + } + } + + Ok(manifests) + } + + fn read_metadata(&self, name: &str) -> Result { + let path = self.layout.metadata_dir().join(format!("{}.json", name)); + let content = fs::read_to_string(&path)?; + let metadata: Metadata = serde_json::from_str(&content)?; + + Ok(metadata) + } +} diff --git a/purescript-registry/src/types.rs b/purescript-registry/src/types.rs new file mode 100644 index 00000000..bcc4656f --- /dev/null +++ b/purescript-registry/src/types.rs @@ -0,0 +1,79 @@ +//! PureScript Registry schema types. +//! +//! These types are schema-complete to match the registry format. +//! Some fields may be unused by specific consumers. +#![allow(dead_code)] + +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Clone, Deserialize)] +pub struct PackageSet { + pub version: String, + pub compiler: String, + pub published: String, + #[serde(flatten)] + pub packages: PackageSetPackages, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PackageSetPackages { + pub packages: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Manifest { + pub name: String, + pub version: semver::Version, + pub license: String, + #[serde(default)] + pub description: Option, + pub location: Location, + #[serde(default)] + pub dependencies: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum Location { + GitHub { + #[serde(rename = "githubOwner")] + owner: String, + #[serde(rename = "githubRepo")] + repo: String, + subdir: Option, + }, + Git { + #[serde(rename = "gitUrl")] + url: String, + subdir: Option, + }, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Metadata { + pub location: Location, + pub owners: Option>, + pub published: HashMap, + #[serde(default)] + pub unpublished: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PublishedVersion { + pub hash: String, + pub ref_: Option, + pub bytes: Option, + #[serde(rename = "publishedTime")] + pub published_time: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct UnpublishedVersion { + pub ref_: Option, + pub reason: String, + #[serde(rename = "publishedTime")] + pub published_time: String, + #[serde(rename = "unpublishedTime")] + pub unpublished_time: String, +} From bb77326f173e2faded9a498b29032e0acbfdf3f3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 00:39:43 +0800 Subject: [PATCH 057/386] Format files and fix small things --- compiler-compatibility/src/registry.rs | 2 +- compiler-compatibility/src/resolver.rs | 10 +++------- compiler-compatibility/src/storage.rs | 21 +++++++++++++++++---- compiler-compatibility/src/types.rs | 4 ++-- compiler-compatibility/src/unpacker.rs | 5 ++++- purescript-registry/src/layout.rs | 5 ++++- purescript-registry/src/lib.rs | 5 ++++- purescript-registry/src/reader.rs | 15 ++++++++------- 8 files changed, 43 insertions(+), 24 deletions(-) diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs index abb3b8f7..e3b681e7 100644 --- a/compiler-compatibility/src/registry.rs +++ b/compiler-compatibility/src/registry.rs @@ -1,6 +1,6 @@ use std::fs; -use git2::{build::RepoBuilder, FetchOptions, Repository}; +use git2::{FetchOptions, Repository, build::RepoBuilder}; use purescript_registry::FsRegistry; use crate::{error::Result, layout::Layout}; diff --git a/compiler-compatibility/src/resolver.rs b/compiler-compatibility/src/resolver.rs index 7fe9dec7..725a336b 100644 --- a/compiler-compatibility/src/resolver.rs +++ b/compiler-compatibility/src/resolver.rs @@ -44,13 +44,9 @@ fn resolve_recursive( let manifests = registry.read_manifest_versions(name)?; let parsed_version: Version = version.parse()?; - let manifest = manifests - .iter() - .find(|m| m.version == parsed_version) - .ok_or_else(|| CompatError::ManifestNotFound { - name: name.to_string(), - version: version.to_string(), - })?; + let manifest = manifests.iter().find(|m| m.version == parsed_version).ok_or_else(|| { + CompatError::ManifestNotFound { name: name.to_string(), version: version.to_string() } + })?; for (dep_name, range) in &manifest.dependencies { let dep_version = package_set diff --git a/compiler-compatibility/src/storage.rs b/compiler-compatibility/src/storage.rs index a210f5ce..6c209d99 100644 --- a/compiler-compatibility/src/storage.rs +++ b/compiler-compatibility/src/storage.rs @@ -11,7 +11,12 @@ pub fn tarball_url(name: &str, version: &str) -> String { format!("https://packages.registry.purescript.org/{}/{}.tar.gz", name, version) } -pub fn fetch_tarball(name: &str, version: &str, layout: &Layout, no_cache: bool) -> Result { +pub fn fetch_tarball( + name: &str, + version: &str, + layout: &Layout, + no_cache: bool, +) -> Result { let tarball_path = layout.tarball_cache_path(name, version); if !no_cache && tarball_path.exists() { @@ -31,8 +36,13 @@ pub fn fetch_tarball(name: &str, version: &str, layout: &Layout, no_cache: bool) Ok(tarball_path) } -pub fn verify_tarball(path: &PathBuf, expected_sha256: &str, name: &str, version: &str) -> Result<()> { - use base64::{engine::general_purpose::STANDARD, Engine}; +pub fn verify_tarball( + path: &PathBuf, + expected_sha256: &str, + name: &str, + version: &str, +) -> Result<()> { + use base64::{Engine, engine::general_purpose::STANDARD}; let mut file = fs::File::open(path)?; let mut hasher = Sha256::new(); @@ -45,7 +55,10 @@ pub fn verify_tarball(path: &PathBuf, expected_sha256: &str, name: &str, version let expected_b64 = expected_sha256.strip_prefix("sha256-").unwrap_or(expected_sha256); if hash_b64 != expected_b64 { - return Err(CompatError::HashMismatch { name: name.to_string(), version: version.to_string() }); + return Err(CompatError::HashMismatch { + name: name.to_string(), + version: version.to_string(), + }); } Ok(()) diff --git a/compiler-compatibility/src/types.rs b/compiler-compatibility/src/types.rs index 4b4aea4d..6140dd08 100644 --- a/compiler-compatibility/src/types.rs +++ b/compiler-compatibility/src/types.rs @@ -20,8 +20,8 @@ pub struct CompatConfig { } impl Default for CompatConfig { - fn default() -> Self { - Self { + fn default() -> CompatConfig { + CompatConfig { root: PathBuf::from("target/compiler-compatibility"), no_cache: false, update_repos: false, diff --git a/compiler-compatibility/src/unpacker.rs b/compiler-compatibility/src/unpacker.rs index 66e9d6db..8b7d02fa 100644 --- a/compiler-compatibility/src/unpacker.rs +++ b/compiler-compatibility/src/unpacker.rs @@ -24,7 +24,10 @@ pub fn unpack_tarball(tarball: &Path, dest_dir: &Path) -> Result { } if path.is_absolute() { - return Err(CompatError::Other(format!("absolute path in archive: {}", path.display()))); + return Err(CompatError::Other(format!( + "absolute path in archive: {}", + path.display() + ))); } entry.unpack_in(dest_dir)?; diff --git a/purescript-registry/src/layout.rs b/purescript-registry/src/layout.rs index 2b3cacd7..cd1203b5 100644 --- a/purescript-registry/src/layout.rs +++ b/purescript-registry/src/layout.rs @@ -12,7 +12,10 @@ pub struct RegistryLayout { impl RegistryLayout { pub fn new(registry_dir: impl AsRef, index_dir: impl AsRef) -> RegistryLayout { - RegistryLayout { registry_dir: registry_dir.as_ref().to_path_buf(), index_dir: index_dir.as_ref().to_path_buf() } + RegistryLayout { + registry_dir: registry_dir.as_ref().to_path_buf(), + index_dir: index_dir.as_ref().to_path_buf(), + } } pub fn package_sets_dir(&self) -> PathBuf { diff --git a/purescript-registry/src/lib.rs b/purescript-registry/src/lib.rs index a9fbc9d9..3248f816 100644 --- a/purescript-registry/src/lib.rs +++ b/purescript-registry/src/lib.rs @@ -6,4 +6,7 @@ mod types; pub use error::{RegistryError, Result}; pub use layout::RegistryLayout; pub use reader::{FsRegistry, RegistryReader}; -pub use types::{Location, Manifest, Metadata, PackageSet, PackageSetPackages, PublishedVersion, UnpublishedVersion}; +pub use types::{ + Location, Manifest, Metadata, PackageSet, PackageSetPackages, PublishedVersion, + UnpublishedVersion, +}; diff --git a/purescript-registry/src/reader.rs b/purescript-registry/src/reader.rs index d5e939c2..a76c8ae9 100644 --- a/purescript-registry/src/reader.rs +++ b/purescript-registry/src/reader.rs @@ -36,12 +36,11 @@ impl RegistryReader for FsRegistry { let entry = entry?; let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "json") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - if let Ok(version) = stem.parse::() { - versions.push(version); - } - } + if path.extension().is_some_and(|ext| ext == "json") + && let Some(stem) = path.file_stem().and_then(|s| s.to_str()) + && let Ok(version) = stem.parse::() + { + versions.push(version); } } @@ -57,7 +56,9 @@ impl RegistryReader for FsRegistry { fn read_package_set(&self, version: Option<&str>) -> Result { let version = match version { Some(v) => v.to_string(), - None => self.list_package_sets()?.into_iter().next().ok_or(RegistryError::NoPackageSets)?, + None => { + self.list_package_sets()?.into_iter().next().ok_or(RegistryError::NoPackageSets)? + } }; let path = self.layout.package_sets_dir().join(format!("{}.json", version)); From 31ebff66d1dcf998be3fd3bfab7771db7a61b6cc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 01:45:13 +0800 Subject: [PATCH 058/386] Add initial package checking with unified tracing Amp-Thread-ID: https://ampcode.com/threads/T-019c05a8-4dde-74b5-8ccb-d74db6770c79 Co-authored-by: Amp --- Cargo.lock | 7 + compiler-compatibility/Cargo.toml | 9 +- compiler-compatibility/src/compat.rs | 218 ++++++++++++++++++++++++++- compiler-compatibility/src/loader.rs | 67 ++++++++ compiler-compatibility/src/main.rs | 62 +++++++- compiler-compatibility/src/trace.rs | 162 ++++++++++++++++++++ tests-integration/src/trace.rs | 5 +- 7 files changed, 517 insertions(+), 13 deletions(-) create mode 100644 compiler-compatibility/src/loader.rs create mode 100644 compiler-compatibility/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index b9c48e20..f86a3a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,19 +386,26 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" name = "compiler-compatibility" version = "0.1.0" dependencies = [ + "analyzer", "base64", "clap", + "diagnostics", + "files", "flate2", "git2", + "glob", "hex", + "line-index", "purescript-registry", "reqwest", + "rowan", "semver", "sha2", "tar", "thiserror", "tracing", "tracing-subscriber", + "url", ] [[package]] diff --git a/compiler-compatibility/Cargo.toml b/compiler-compatibility/Cargo.toml index 53ff1baa..9f74c6b7 100644 --- a/compiler-compatibility/Cargo.toml +++ b/compiler-compatibility/Cargo.toml @@ -5,6 +5,9 @@ edition = "2024" [dependencies] purescript-registry = { path = "../purescript-registry" } +analyzer = { path = "../compiler-lsp/analyzer" } +files = { path = "../compiler-core/files" } +diagnostics = { path = "../compiler-core/diagnostics" } clap = { version = "4", features = ["derive"] } git2 = "0.20" reqwest = { version = "0.12", features = ["blocking"] } @@ -15,5 +18,9 @@ sha2 = "0.10" hex = "0.4" thiserror = "2" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } base64 = "0.22.1" +glob = "0.3" +url = "2" +line-index = "0.1" +rowan = "0.16" diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/src/compat.rs index faad7411..33adba13 100644 --- a/compiler-compatibility/src/compat.rs +++ b/compiler-compatibility/src/compat.rs @@ -1,5 +1,219 @@ +//! Package compatibility checking with QueryEngine. + use std::path::Path; -pub fn check_package(_package_dir: &Path) -> Vec { - todo!("Track D: QueryEngine integration scaffold") +use analyzer::QueryEngine; +use diagnostics::{DiagnosticsContext, Severity, ToDiagnostics}; +use files::{FileId, Files}; +use line_index::LineIndex; +use rowan::TextSize; + +use tracing::field; +use url::Url; + +use crate::loader; + +/// Result of checking a single file. +pub struct FileResult { + pub relative_path: String, + pub error_count: usize, + pub warning_count: usize, + pub output: String, +} + +/// Result of checking a package. +pub struct CheckResult { + pub files: Vec, + pub total_errors: usize, + pub total_warnings: usize, +} + +/// Checks all packages in the packages directory and returns diagnostics. +/// +/// `packages_dir` should point to the directory containing unpacked packages. +/// `target_package` is the specific package to report diagnostics for (others are loaded as deps). +pub fn check_package(packages_dir: &Path, target_package: &str) -> CheckResult { + let _span = + tracing::info_span!(target: "compiler_compatibility", "check_package", target_package) + .entered(); + + let (engine, files, file_ids) = loader::load_packages(packages_dir); + + let target_directory = find_package_dir(packages_dir, target_package); + let target_files = if let Some(directory) = &target_directory { + loader::filter_package_files(&files, &file_ids, directory) + } else { + vec![] + }; + + let mut results = Vec::new(); + let mut total_errors = 0; + let mut total_warnings = 0; + + tracing::info!(target: "compiler_compatibility", count = target_files.len()); + + for id in target_files { + let relative_path = compute_relative_path(&files, id, packages_dir); + let file_result = check_file(&engine, &files, id, &relative_path); + + total_errors += file_result.error_count; + total_warnings += file_result.warning_count; + results.push(file_result); + } + + CheckResult { files: results, total_errors, total_warnings } +} + +fn find_package_dir(packages_dir: &Path, package_name: &str) -> Option { + let entries = std::fs::read_dir(packages_dir).ok()?; + for entry in entries.filter_map(Result::ok) { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with(package_name) + && name_str[package_name.len()..].starts_with('-') + && entry.file_type().ok()?.is_dir() + { + return entry.path().canonicalize().ok(); + } + } + None +} + +fn compute_relative_path(files: &Files, id: FileId, base_dir: &Path) -> String { + let uri = files.path(id); + Url::parse(&uri) + .ok() + .and_then(|u| u.to_file_path().ok()) + .and_then(|p| p.strip_prefix(base_dir).ok().map(|r| r.to_path_buf())) + .map(|p| p.display().to_string()) + .unwrap_or_else(|| uri.to_string()) +} + +fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: &str) -> FileResult { + let _span = tracing::info_span!(target: "compiler_compatibility", "check_file").entered(); + + let content = engine.content(id); + let line_index = LineIndex::new(&content); + + let Ok((parsed, _)) = engine.parsed(id) else { + return FileResult { + relative_path: relative_path.to_string(), + error_count: 1, + warning_count: 0, + output: format!("{relative_path}:1:1: error[ParseError]: Failed to parse file\n"), + }; + }; + + if let Some(module_name) = parsed.module_name() { + tracing::info!(target: "compiler_compatibility", module_name = %module_name); + } else { + tracing::warn!(target: "compiler_compatibility", path = ?relative_path, "Invalid module name"); + }; + + let root = parsed.syntax_node(); + let stabilized = match engine.stabilized(id) { + Ok(s) => s, + Err(_) => { + return FileResult { + relative_path: relative_path.to_string(), + error_count: 1, + warning_count: 0, + output: format!( + "{relative_path}:1:1: error[StabilizeError]: Failed to stabilize\n" + ), + }; + } + }; + + let indexed = match engine.indexed(id) { + Ok(i) => i, + Err(_) => { + return FileResult { + relative_path: relative_path.to_string(), + error_count: 1, + warning_count: 0, + output: format!("{relative_path}:1:1: error[IndexError]: Failed to index\n"), + }; + } + }; + + let lowered = engine.lowered(id); + let resolved = engine.resolved(id); + let checked = engine.checked(id); + + let checked_ref = match &checked { + Ok(c) => c, + Err(_) => { + return FileResult { + relative_path: relative_path.to_string(), + error_count: 1, + warning_count: 0, + output: format!("{relative_path}:1:1: error[CheckError]: Failed to check\n"), + }; + } + }; + + let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, checked_ref); + + let mut all_diagnostics = vec![]; + + if let Ok(ref lowered) = lowered { + for error in &lowered.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } + } + + if let Ok(ref resolved) = resolved { + for error in &resolved.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } + } + + for error in &checked_ref.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); + } + + let mut output = String::new(); + let mut error_count = 0; + let mut warning_count = 0; + + for diagnostic in &all_diagnostics { + match diagnostic.severity { + Severity::Error => error_count += 1, + Severity::Warning => warning_count += 1, + } + + let severity = match diagnostic.severity { + Severity::Error => "error", + Severity::Warning => "warning", + }; + + let offset = TextSize::from(diagnostic.primary.start); + let (line, col) = offset_to_line_col(&line_index, &content, offset); + + output.push_str(&format!( + "{relative_path}:{}:{}: {severity}[{}]: {}\n", + line + 1, + col + 1, + diagnostic.code, + diagnostic.message + )); + } + + FileResult { relative_path: relative_path.to_string(), error_count, warning_count, output } +} + +fn offset_to_line_col(line_index: &LineIndex, content: &str, offset: TextSize) -> (u32, u32) { + let line_col = line_index.line_col(offset); + let line = line_col.line; + + let line_range = match line_index.line(line) { + Some(r) => r, + None => return (line, 0), + }; + let line_content = &content[line_range]; + let until_col = &line_content[..line_col.col as usize]; + let character = until_col.chars().count() as u32; + + (line, character) } diff --git a/compiler-compatibility/src/loader.rs b/compiler-compatibility/src/loader.rs new file mode 100644 index 00000000..4fb34598 --- /dev/null +++ b/compiler-compatibility/src/loader.rs @@ -0,0 +1,67 @@ +//! Loads PureScript packages into QueryEngine + Files. + +use std::fs; +use std::path::Path; + +use analyzer::{QueryEngine, prim}; +use files::{FileId, Files}; +use glob::glob; +use url::Url; + +/// Loads all .purs files from a directory into the engine. +fn load_file(engine: &mut QueryEngine, files: &mut Files, path: &Path) -> Option { + let path = path.canonicalize().ok()?; + let url = Url::from_file_path(&path).ok()?; + let content = fs::read_to_string(&path).ok()?; + let content = content.replace("\r\n", "\n"); + + let uri = url.to_string(); + let id = files.insert(uri, content); + let content = files.content(id); + + engine.set_content(id, content); + if let Ok((parsed, _)) = engine.parsed(id) { + if let Some(name) = parsed.module_name() { + engine.set_module_file(&name, id); + } + } + + Some(id) +} + +/// Loads all packages from a packages directory. +/// +/// Returns (engine, files, file_ids) where file_ids contains all loaded .purs files. +pub fn load_packages(packages_dir: &Path) -> (QueryEngine, Files, Vec) { + let mut engine = QueryEngine::default(); + let mut files = Files::default(); + prim::configure(&mut engine, &mut files); + + let pattern = format!("{}/**/*.purs", packages_dir.display()); + let mut paths: Vec<_> = glob(&pattern).into_iter().flatten().filter_map(Result::ok).collect(); + paths.sort(); + + let mut file_ids = Vec::new(); + for path in paths { + if let Some(id) = load_file(&mut engine, &mut files, &path) { + file_ids.push(id); + } + } + + (engine, files, file_ids) +} + +/// Filters file IDs to only those belonging to a specific package directory. +pub fn filter_package_files(files: &Files, file_ids: &[FileId], package_dir: &Path) -> Vec { + file_ids + .iter() + .copied() + .filter(|&id| { + let uri = files.path(id); + Url::parse(&uri) + .ok() + .and_then(|u| u.to_file_path().ok()) + .is_some_and(|p| p.starts_with(package_dir)) + }) + .collect() +} diff --git a/compiler-compatibility/src/main.rs b/compiler-compatibility/src/main.rs index 6ddb10b0..39390568 100644 --- a/compiler-compatibility/src/main.rs +++ b/compiler-compatibility/src/main.rs @@ -1,9 +1,11 @@ mod compat; mod error; mod layout; +mod loader; mod registry; mod resolver; mod storage; +mod trace; mod types; mod unpacker; @@ -11,6 +13,7 @@ use std::path::PathBuf; use clap::Parser; use purescript_registry::RegistryReader; +use tracing::level_filters::LevelFilter; #[derive(Parser, Debug)] #[command(name = "compiler-compatibility")] @@ -31,18 +34,29 @@ struct Cli { #[arg(long, default_value = "target/compiler-compatibility", help = "Output directory")] output: PathBuf, + #[arg(long, default_value = "target/compiler-tracing", help = "Trace output directory")] + trace_output: PathBuf, + #[arg(long, help = "Disable tarball caching")] no_cache: bool, #[arg(short, long, help = "Verbose output")] verbose: bool, + + #[arg( + long, + value_name = "LevelFilter", + default_value = "debug", + help = "Log level for checking crate traces" + )] + log_level: LevelFilter, } fn main() -> error::Result<()> { let cli = Cli::parse(); - let filter = if cli.verbose { "debug" } else { "info" }; - tracing_subscriber::fmt().with_env_filter(filter).init(); + let stdout_level = if cli.verbose { LevelFilter::DEBUG } else { LevelFilter::INFO }; + let tracing_handle = trace::init_tracing(stdout_level, cli.log_level, cli.trace_output); let layout = layout::Layout::new(&cli.output); let registry = registry::Registry::new(layout.clone()); @@ -63,10 +77,10 @@ fn main() -> error::Result<()> { } let package_set = registry.reader().read_package_set(cli.package_set.as_deref())?; - tracing::info!(version = %package_set.version, "Using package set"); + tracing::info!(target: "compiler_compatibility", version = %package_set.version, "Using package set"); let resolved = resolver::resolve(&cli.packages, &package_set, registry.reader())?; - tracing::info!(count = resolved.packages.len(), "Resolved packages"); + tracing::info!(target: "compiler_compatibility", count = resolved.packages.len(), "Resolved packages"); for (name, version) in &resolved.packages { let metadata = registry.reader().read_metadata(name)?; @@ -76,11 +90,45 @@ fn main() -> error::Result<()> { let tarball = storage::fetch_tarball(name, version, &layout, cli.no_cache)?; storage::verify_tarball(&tarball, &published.hash, name, version)?; - unpacker::unpack_tarball(&tarball, &layout.package_dir(name, version))?; + unpacker::unpack_tarball(&tarball, &layout.packages_dir)?; + + tracing::info!(target: "compiler_compatibility", name, version, "Unpacked"); + } + + tracing::info!(target: "compiler_compatibility", directory = %layout.packages_dir.display(), "Finished unpacking"); + + for package in &cli.packages { + let _span = tracing::info_span!(target: "compiler_compatibility", "for_each_package", package).entered(); + + let guard = + tracing_handle.begin_package(package).expect("failed to start package trace capture"); + let log_file = guard.path().to_path_buf(); + + let result = compat::check_package(&layout.packages_dir, package); + + drop(guard); + + for file_result in &result.files { + if !file_result.output.is_empty() { + print!("{}", file_result.output); + } + } + + let summary = format!( + "{}: {} errors, {} warnings", + package, result.total_errors, result.total_warnings + ); + + if result.total_errors > 0 { + tracing::error!(target: "compiler_compatibility", "{}", summary); + } else if result.total_warnings > 0 { + tracing::warn!(target: "compiler_compatibility", "{}", summary); + } else { + tracing::info!(target: "compiler_compatibility", "{}", summary); + } - tracing::info!(name, version, "Unpacked"); + tracing::debug!(target: "compiler_compatibility", path = %log_file.display(), "Trace written"); } - tracing::info!(dir = %layout.packages_dir.display(), "Done"); Ok(()) } diff --git a/compiler-compatibility/src/trace.rs b/compiler-compatibility/src/trace.rs new file mode 100644 index 00000000..a9b4c0ef --- /dev/null +++ b/compiler-compatibility/src/trace.rs @@ -0,0 +1,162 @@ +//! Tracing setup with dynamically switchable file output for per-package logs. + +use std::fs::{self, File}; +use std::io::{self, BufWriter, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use tracing_subscriber::Layer; +use tracing_subscriber::filter::{LevelFilter, Targets}; +use tracing_subscriber::fmt::MakeWriter; +use tracing_subscriber::fmt::format::FmtSpan; +use tracing_subscriber::layer::SubscriberExt; + +struct RouterState { + writer: Option>, + current_path: Option, +} + +#[derive(Clone)] +struct CheckingLogsRouter { + state: Arc>, +} + +struct CheckingLogsWriter { + state: Arc>, +} + +impl CheckingLogsRouter { + fn new() -> CheckingLogsRouter { + CheckingLogsRouter { + state: Arc::new(Mutex::new(RouterState { writer: None, current_path: None })), + } + } + + fn set_writer(&self, writer: BufWriter, path: PathBuf) -> io::Result<()> { + let mut state = self.state.lock().unwrap(); + if let Some(w) = state.writer.as_mut() { + w.flush()?; + } + state.writer = Some(writer); + state.current_path = Some(path); + Ok(()) + } + + fn clear(&self) -> io::Result<()> { + let mut state = self.state.lock().unwrap(); + if let Some(w) = state.writer.as_mut() { + w.flush()?; + } + state.writer = None; + state.current_path = None; + Ok(()) + } +} + +impl Write for CheckingLogsWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let mut state = self.state.lock().unwrap(); + if let Some(w) = state.writer.as_mut() { + w.write(buf) + } else { + Ok(buf.len()) + } + } + + fn flush(&mut self) -> io::Result<()> { + let mut state = self.state.lock().unwrap(); + if let Some(w) = state.writer.as_mut() { + w.flush() + } else { + Ok(()) + } + } +} + +impl<'a> MakeWriter<'a> for CheckingLogsRouter { + type Writer = CheckingLogsWriter; + + fn make_writer(&'a self) -> CheckingLogsWriter { + CheckingLogsWriter { state: self.state.clone() } + } +} + +/// Handle for controlling per-package trace capture. +pub struct TracingHandle { + router: CheckingLogsRouter, + trace_dir: PathBuf, +} + +impl TracingHandle { + /// Begin capturing `checking` crate logs to a new file for the given package. + /// + /// Returns a guard that flushes and closes the file when dropped. + pub fn begin_package(&self, package: &str) -> io::Result { + fs::create_dir_all(&self.trace_dir)?; + + let timestamp = + SystemTime::now().duration_since(UNIX_EPOCH).expect("time before epoch").as_millis(); + + let sanitized: String = + package.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }).collect(); + + let path = self.trace_dir.join(format!("{}_{}.jsonl", timestamp, sanitized)); + let file = File::create(&path)?; + let writer = BufWriter::new(file); + + self.router.set_writer(writer, path.clone())?; + + Ok(PackageTraceGuard { router: self.router.clone(), path }) + } +} + +/// Guard that flushes and closes the trace file on drop. +pub struct PackageTraceGuard { + router: CheckingLogsRouter, + path: PathBuf, +} + +impl PackageTraceGuard { + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for PackageTraceGuard { + fn drop(&mut self) { + let _ = self.router.clear(); + } +} + +/// Initialize global tracing with dual outputs: +/// - `compiler_compatibility` logs → stdout at `stdout_level` +/// - `checking` logs → per-package JSONL files at `checking_level` +/// +/// Returns a handle for switching the file output between packages. +pub fn init_tracing( + stdout_level: LevelFilter, + checking_level: LevelFilter, + trace_dir: PathBuf, +) -> TracingHandle { + let router = CheckingLogsRouter::new(); + + let stdout_filter = + Targets::new().with_target("compiler_compatibility", stdout_level).with_default(LevelFilter::OFF); + let stdout_layer = tracing_subscriber::fmt::layer().with_filter(stdout_filter); + + let file_filter = + Targets::new().with_target("checking", checking_level).with_default(LevelFilter::OFF); + let file_layer = tracing_subscriber::fmt::layer() + .with_writer(router.clone()) + .json() + .with_span_events(FmtSpan::CLOSE) + .with_filter(file_filter); + + let subscriber = tracing_subscriber::registry().with(stdout_layer).with(file_layer); + + tracing::subscriber::set_global_default(subscriber) + .expect("failed to set global tracing subscriber"); + + TracingHandle { router, trace_dir } +} diff --git a/tests-integration/src/trace.rs b/tests-integration/src/trace.rs index 584fca37..6118c869 100644 --- a/tests-integration/src/trace.rs +++ b/tests-integration/src/trace.rs @@ -51,9 +51,8 @@ where .with_span_events(FmtSpan::CLOSE) .json(); - let subscriber = tracing_subscriber::registry() - .with(formatter) - .with(EnvFilter::default().add_directive(level.into())); + let filter = EnvFilter::default().add_directive(level.into()); + let subscriber = tracing_subscriber::registry().with(formatter).with(filter); let result = tracing::subscriber::with_default(subscriber, f); file_writer.0.lock().unwrap().flush().expect("failed to flush trace file"); From 7079b1d7140c46e38d35edaa8502d29f2ffe5680 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 03:47:52 +0800 Subject: [PATCH 059/386] Prevent elaboration on contravariant positions This implements elaboration modes for the type checker, which solves an issue with constraints appearing in contravariant positions. Since the type checker is syntax driven, this also disables elaboration for binders and expressions that appear in argument positions. Amp-Thread-ID: https://ampcode.com/threads/T-019c0609-d881-716e-90f2-bf0849775d80 Co-authored-by: Amp --- .../checking/src/algorithm/binder.rs | 107 ++++++++++++++---- compiler-core/checking/src/algorithm/term.rs | 28 ++++- .../checking/src/algorithm/unification.rs | 91 ++++++++++++--- .../254_higher_rank_elaboration/Main.purs | 21 ++++ .../254_higher_rank_elaboration/Main.snap | 31 +++++ tests-integration/tests/checking/generated.rs | 2 + 6 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.purs create mode 100644 tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index 02750841..466395c7 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -3,6 +3,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{binder, kind, operator, term, toolkit, unification}; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::ErrorStep; @@ -10,7 +11,7 @@ use crate::error::ErrorStep; #[derive(Copy, Clone, Debug)] enum BinderMode { Infer, - Check { expected_type: TypeId }, + Check { expected_type: TypeId, elaboration: ElaborationMode }, } pub fn infer_binder( @@ -35,8 +36,24 @@ pub fn check_binder( where Q: ExternalQueries, { + let elaboration = ElaborationMode::Yes; state.with_error_step(ErrorStep::CheckingBinder(binder_id), |state| { - binder_core(state, context, binder_id, BinderMode::Check { expected_type }) + binder_core(state, context, binder_id, BinderMode::Check { expected_type, elaboration }) + }) +} + +pub fn check_argument_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + expected_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let elaboration = ElaborationMode::No; + state.with_error_step(ErrorStep::CheckingBinder(binder_id), |state| { + binder_core(state, context, binder_id, BinderMode::Check { expected_type, elaboration }) }) } @@ -61,10 +78,17 @@ where let Some(t) = type_ else { return Ok(unknown) }; let (t, _) = kind::infer_surface_kind(state, context, *t)?; - check_binder(state, context, *b, t)?; + match mode { + BinderMode::Check { elaboration: ElaborationMode::No, .. } => { + check_argument_binder(state, context, *b, t)?; + } + _ => { + check_binder(state, context, *b, t)?; + } + } - if let BinderMode::Check { expected_type } = mode { - unification::subtype(state, context, t, expected_type)?; + if let BinderMode::Check { expected_type, elaboration } = mode { + unification::subtype_with_mode(state, context, t, expected_type, elaboration)?; } Ok(t) @@ -73,8 +97,14 @@ where lowering::BinderKind::OperatorChain { .. } => { let (_, inferred_type) = operator::infer_operator_chain(state, context, binder_id)?; - if let BinderMode::Check { expected_type } = mode { - unification::subtype(state, context, inferred_type, expected_type)?; + if let BinderMode::Check { expected_type, elaboration } = mode { + unification::subtype_with_mode( + state, + context, + inferred_type, + expected_type, + elaboration, + )?; } Ok(inferred_type) @@ -83,7 +113,7 @@ where lowering::BinderKind::Integer => { let inferred_type = context.prim.int; - if let BinderMode::Check { expected_type } = mode { + if let BinderMode::Check { expected_type, .. } = mode { unification::unify(state, context, inferred_type, expected_type)?; } @@ -93,7 +123,7 @@ where lowering::BinderKind::Number => { let inferred_type = context.prim.number; - if let BinderMode::Check { expected_type } = mode { + if let BinderMode::Check { expected_type, .. } = mode { unification::unify(state, context, inferred_type, expected_type)?; } @@ -122,8 +152,14 @@ where constructor_t }; - if let BinderMode::Check { expected_type } = mode { - unification::subtype(state, context, inferred_type, expected_type)?; + if let BinderMode::Check { expected_type, elaboration } = mode { + unification::subtype_with_mode( + state, + context, + inferred_type, + expected_type, + elaboration, + )?; Ok(expected_type) } else { Ok(inferred_type) @@ -133,7 +169,7 @@ where lowering::BinderKind::Variable { .. } => { let type_id = match mode { BinderMode::Infer => state.fresh_unification_type(context), - BinderMode::Check { expected_type } => expected_type, + BinderMode::Check { expected_type, .. } => expected_type, }; state.term_scope.bind_binder(binder_id, type_id); Ok(type_id) @@ -144,9 +180,14 @@ where let type_id = match mode { BinderMode::Infer => infer_binder(state, context, *binder)?, - BinderMode::Check { expected_type } => { - check_binder(state, context, *binder, expected_type)? - } + BinderMode::Check { expected_type, elaboration } => match elaboration { + ElaborationMode::Yes => { + check_binder(state, context, *binder, expected_type)? + } + ElaborationMode::No => { + check_argument_binder(state, context, *binder, expected_type)? + } + }, }; state.term_scope.bind_binder(binder_id, type_id); @@ -155,13 +196,13 @@ where lowering::BinderKind::Wildcard => match mode { BinderMode::Infer => Ok(state.fresh_unification_type(context)), - BinderMode::Check { expected_type } => Ok(expected_type), + BinderMode::Check { expected_type, .. } => Ok(expected_type), }, lowering::BinderKind::String => { let inferred_type = context.prim.string; - if let BinderMode::Check { expected_type } = mode { + if let BinderMode::Check { expected_type, .. } = mode { unification::unify(state, context, inferred_type, expected_type)?; } @@ -171,7 +212,7 @@ where lowering::BinderKind::Char => { let inferred_type = context.prim.char; - if let BinderMode::Check { expected_type } = mode { + if let BinderMode::Check { expected_type, .. } = mode { unification::unify(state, context, inferred_type, expected_type)?; } @@ -181,7 +222,7 @@ where lowering::BinderKind::Boolean { .. } => { let inferred_type = context.prim.boolean; - if let BinderMode::Check { expected_type } = mode { + if let BinderMode::Check { expected_type, .. } = mode { unification::unify(state, context, inferred_type, expected_type)?; } @@ -193,14 +234,26 @@ where for binder in array.iter() { let binder_type = infer_binder(state, context, *binder)?; - unification::subtype(state, context, binder_type, element_type)?; + unification::subtype_with_mode( + state, + context, + binder_type, + element_type, + ElaborationMode::No, + )?; } let array_type = state.storage.intern(Type::Application(context.prim.array, element_type)); - if let BinderMode::Check { expected_type } = mode { - unification::subtype(state, context, array_type, expected_type)?; + if let BinderMode::Check { expected_type, elaboration } = mode { + unification::subtype_with_mode( + state, + context, + array_type, + expected_type, + elaboration, + )?; } Ok(array_type) @@ -239,8 +292,14 @@ where let record_type = state.storage.intern(Type::Application(context.prim.record, row_type)); - if let BinderMode::Check { expected_type } = mode { - unification::subtype(state, context, record_type, expected_type)?; + if let BinderMode::Check { expected_type, elaboration } = mode { + unification::subtype_with_mode( + state, + context, + record_type, + expected_type, + elaboration, + )?; Ok(expected_type) } else { Ok(record_type) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 83f234d0..d707d28d 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -7,6 +7,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{ binder, inspect, kind, operator, substitute, toolkit, transfer, unification, }; @@ -129,7 +130,7 @@ where } for (&binder_id, &argument_type) in equation.binders.iter().zip(&signature.arguments) { - let _ = binder::check_binder(state, context, binder_id, argument_type)?; + let _ = binder::check_argument_binder(state, context, binder_id, argument_type)?; } if equation_arity > expected_arity { @@ -293,6 +294,29 @@ where }) } +fn check_expression_argument( + state: &mut CheckState, + context: &CheckContext, + expr_id: lowering::ExpressionId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + state.with_error_step(ErrorStep::CheckingExpression(expr_id), |state| { + let inferred = infer_expression_quiet(state, context, expr_id)?; + unification::subtype_with_mode( + state, + context, + inferred, + expected, + ElaborationMode::No, + )?; + crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); + Ok(inferred) + }) +} + /// Infers the type of an expression. #[tracing::instrument(skip_all, name = "infer_expression")] pub fn infer_expression( @@ -1796,7 +1820,7 @@ pub fn check_function_term_application( where Q: ExternalQueries, { - check_function_application_core(state, context, function_t, expression_id, check_expression) + check_function_application_core(state, context, function_t, expression_id, check_expression_argument) } fn check_let_chunks( diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index ace49113..474afee5 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -10,6 +10,30 @@ use crate::algorithm::{kind, substitute}; use crate::core::{RowField, RowType, Type, TypeId, Variable, debruijn}; use crate::error::ErrorKind; +/// Determines if constraints are elaborated during [`subtype`]. +/// +/// Elaboration means pushing constraints as "wanted" and inserting dictionary +/// placeholders. This is only valid in **covariant** positions where the type +/// checker controls what value is passed. +/// +/// In **contravariant** positions (e.g., function arguments), the caller provides +/// values—we cannot insert dictionaries there. When both sides have matching +/// constraint structure, structural unification handles them correctly: +/// +/// ```text +/// (IsSymbol ?sym => Proxy ?sym -> r) <= (IsSymbol ~sym => Proxy ~sym -> r) +/// IsSymbol ?sym ~ IsSymbol ~sym → solves ?sym := ~sym +/// ``` +/// +/// [`Type::Function`] in the [`subtype`] rule disables this for the argument +/// and result positions. Syntax-driven rules like checking for binders and +/// expressions that appear in the argument position also disable elaboration. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ElaborationMode { + Yes, + No, +} + /// Check that `t1` is a subtype of `t2` /// /// In the type system, we define that polymorphic types are subtypes of @@ -42,20 +66,33 @@ use crate::error::ErrorKind; /// subtype (?a -> ?a) (~a -> ~a) /// subtype ?a ~a /// ``` -#[tracing::instrument(skip_all, name = "subtype")] pub fn subtype( state: &mut CheckState, context: &CheckContext, t1: TypeId, t2: TypeId, ) -> QueryResult +where + Q: ExternalQueries, +{ + subtype_with_mode(state, context, t1, t2, ElaborationMode::Yes) +} + +#[tracing::instrument(skip_all, name = "subtype_with_mode")] +pub fn subtype_with_mode( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, + mode: ElaborationMode, +) -> QueryResult where Q: ExternalQueries, { let t1 = synonym::normalize_expand_type(state, context, t1)?; let t2 = synonym::normalize_expand_type(state, context, t2)?; - crate::debug_fields!(state, context, { t1 = t1, t2 = t2 }); + crate::debug_fields!(state, context, { t1 = t1, t2 = t2, ?mode = mode }); if t1 == t2 { crate::trace_fields!(state, context, { t1 = t1, t2 = t2 }, "identical"); @@ -67,16 +104,28 @@ where match (t1_core, t2_core) { (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { - Ok(subtype(state, context, t2_argument, t1_argument)? - && subtype(state, context, t1_result, t2_result)?) + Ok(subtype_with_mode(state, context, t2_argument, t1_argument, ElaborationMode::No)? + && subtype_with_mode(state, context, t1_result, t2_result, ElaborationMode::No)?) } (Type::Application(t1_partial, t1_result), Type::Function(t2_argument, t2_result)) => { let t1_partial = state.normalize_type(t1_partial); if let Type::Application(t1_constructor, t1_argument) = state.storage[t1_partial] { Ok(unify(state, context, t1_constructor, context.prim.function)? - && subtype(state, context, t2_argument, t1_argument)? - && subtype(state, context, t1_result, t2_result)?) + && subtype_with_mode( + state, + context, + t2_argument, + t1_argument, + ElaborationMode::No, + )? + && subtype_with_mode( + state, + context, + t1_result, + t2_result, + ElaborationMode::No, + )?) } else { unify(state, context, t1, t2) } @@ -86,8 +135,20 @@ where let t2_partial = state.normalize_type(t2_partial); if let Type::Application(t2_constructor, t2_argument) = state.storage[t2_partial] { Ok(unify(state, context, t2_constructor, context.prim.function)? - && subtype(state, context, t2_argument, t1_argument)? - && subtype(state, context, t1_result, t2_result)?) + && subtype_with_mode( + state, + context, + t2_argument, + t1_argument, + ElaborationMode::No, + )? + && subtype_with_mode( + state, + context, + t1_result, + t2_result, + ElaborationMode::No, + )?) } else { unify(state, context, t1, t2) } @@ -98,7 +159,7 @@ where let t = state.storage.intern(Type::Variable(v)); let inner = substitute::SubstituteBound::on(state, binder.level, t, inner); - subtype(state, context, t1, inner) + subtype_with_mode(state, context, t1, inner, mode) } (Type::Forall(ref binder, inner), _) => { @@ -106,12 +167,12 @@ where let t = state.fresh_unification_kinded(k); let inner = substitute::SubstituteBound::on(state, binder.level, t, inner); - subtype(state, context, inner, t2) + subtype_with_mode(state, context, inner, t2, mode) } - (Type::Constrained(constraint, inner), _) => { + (Type::Constrained(constraint, inner), _) if mode == ElaborationMode::Yes => { state.constraints.push_wanted(constraint); - subtype(state, context, inner, t2) + subtype_with_mode(state, context, inner, t2, mode) } ( @@ -125,7 +186,7 @@ where let t2_core = state.storage[t2_argument].clone(); if let (Type::Row(t1_row), Type::Row(t2_row)) = (t1_core, t2_core) { - subtype_record_rows(state, context, &t1_row, &t2_row) + subtype_record_rows(state, context, &t1_row, &t2_row, mode) } else { unify(state, context, t1, t2) } @@ -439,6 +500,7 @@ fn subtype_record_rows( context: &CheckContext, t1_row: &RowType, t2_row: &RowType, + mode: ElaborationMode, ) -> QueryResult where Q: ExternalQueries, @@ -449,7 +511,8 @@ where t1_row, t2_row, |state, context, left, right| { - Ok(subtype(state, context, left, right)? && subtype(state, context, right, left)?) + Ok(subtype_with_mode(state, context, left, right, mode)? + && subtype_with_mode(state, context, right, left, mode)?) }, )?; diff --git a/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.purs b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.purs new file mode 100644 index 00000000..835832bd --- /dev/null +++ b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.purs @@ -0,0 +1,21 @@ +module Main where + +class IsSymbol (sym :: Symbol) where + reflectSymbol :: Proxy sym -> String + +data Proxy (a :: Symbol) = Proxy + +reifySymbol :: forall r. String -> (forall sym. IsSymbol sym => Proxy sym -> r) -> r +reifySymbol s f = coerce f { reflectSymbol: \_ -> s } Proxy + where + coerce + :: (forall sym1. IsSymbol sym1 => Proxy sym1 -> r) + -> { reflectSymbol :: Proxy "" -> String } + -> Proxy "" + -> r + coerce = unsafeCoerce + +foreign import unsafeCoerce :: forall a b. a -> b + +test :: String -> String +test s = reifySymbol s reflectSymbol diff --git a/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap new file mode 100644 index 00000000..2d9858e1 --- /dev/null +++ b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +reflectSymbol :: forall (sym :: Symbol). IsSymbol (sym :: Symbol) => Proxy (sym :: Symbol) -> String +Proxy :: forall (a :: Symbol). Proxy (a :: Symbol) +reifySymbol :: + forall (r :: Type). + String -> + (forall (sym :: Symbol). IsSymbol (sym :: Symbol) => Proxy (sym :: Symbol) -> (r :: Type)) -> + (r :: Type) +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +test :: String -> String + +Types +IsSymbol :: Symbol -> Constraint +Proxy :: Symbol -> Type + +Data +Proxy + Quantified = :0 + Kind = :0 + + +Roles +Proxy = [Phantom] + +Classes +class IsSymbol (&0 :: Symbol) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 36d2a0f7..d8d27747 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -521,3 +521,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_252_invalid_type_application_basic_main() { run_test("252_invalid_type_application_basic", "Main"); } #[rustfmt::skip] #[test] fn test_253_invalid_type_application_too_many_main() { run_test("253_invalid_type_application_too_many", "Main"); } + +#[rustfmt::skip] #[test] fn test_254_higher_rank_elaboration_main() { run_test("254_higher_rank_elaboration", "Main"); } From 0f825301a3840faa4458fa92d4bf025d7971887f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 03:52:13 +0800 Subject: [PATCH 060/386] Format files --- compiler-compatibility/src/main.rs | 4 +++- compiler-compatibility/src/trace.rs | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/compiler-compatibility/src/main.rs b/compiler-compatibility/src/main.rs index 39390568..b29b19c7 100644 --- a/compiler-compatibility/src/main.rs +++ b/compiler-compatibility/src/main.rs @@ -98,7 +98,9 @@ fn main() -> error::Result<()> { tracing::info!(target: "compiler_compatibility", directory = %layout.packages_dir.display(), "Finished unpacking"); for package in &cli.packages { - let _span = tracing::info_span!(target: "compiler_compatibility", "for_each_package", package).entered(); + let _span = + tracing::info_span!(target: "compiler_compatibility", "for_each_package", package) + .entered(); let guard = tracing_handle.begin_package(package).expect("failed to start package trace capture"); diff --git a/compiler-compatibility/src/trace.rs b/compiler-compatibility/src/trace.rs index a9b4c0ef..0765ee57 100644 --- a/compiler-compatibility/src/trace.rs +++ b/compiler-compatibility/src/trace.rs @@ -57,20 +57,12 @@ impl CheckingLogsRouter { impl Write for CheckingLogsWriter { fn write(&mut self, buf: &[u8]) -> io::Result { let mut state = self.state.lock().unwrap(); - if let Some(w) = state.writer.as_mut() { - w.write(buf) - } else { - Ok(buf.len()) - } + if let Some(w) = state.writer.as_mut() { w.write(buf) } else { Ok(buf.len()) } } fn flush(&mut self) -> io::Result<()> { let mut state = self.state.lock().unwrap(); - if let Some(w) = state.writer.as_mut() { - w.flush() - } else { - Ok(()) - } + if let Some(w) = state.writer.as_mut() { w.flush() } else { Ok(()) } } } @@ -98,8 +90,10 @@ impl TracingHandle { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("time before epoch").as_millis(); - let sanitized: String = - package.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }).collect(); + let sanitized: String = package + .chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }) + .collect(); let path = self.trace_dir.join(format!("{}_{}.jsonl", timestamp, sanitized)); let file = File::create(&path)?; @@ -141,8 +135,9 @@ pub fn init_tracing( ) -> TracingHandle { let router = CheckingLogsRouter::new(); - let stdout_filter = - Targets::new().with_target("compiler_compatibility", stdout_level).with_default(LevelFilter::OFF); + let stdout_filter = Targets::new() + .with_target("compiler_compatibility", stdout_level) + .with_default(LevelFilter::OFF); let stdout_layer = tracing_subscriber::fmt::layer().with_filter(stdout_filter); let file_filter = From 4ffce6716b42b7545f033789cfac896dc785cb48 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 29 Jan 2026 18:51:08 +0800 Subject: [PATCH 061/386] Format files --- compiler-core/checking/src/algorithm/binder.rs | 4 +--- compiler-core/checking/src/algorithm/term.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index 466395c7..0ee5f9b3 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -181,9 +181,7 @@ where let type_id = match mode { BinderMode::Infer => infer_binder(state, context, *binder)?, BinderMode::Check { expected_type, elaboration } => match elaboration { - ElaborationMode::Yes => { - check_binder(state, context, *binder, expected_type)? - } + ElaborationMode::Yes => check_binder(state, context, *binder, expected_type)?, ElaborationMode::No => { check_argument_binder(state, context, *binder, expected_type)? } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index d707d28d..45f00790 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -305,13 +305,7 @@ where { state.with_error_step(ErrorStep::CheckingExpression(expr_id), |state| { let inferred = infer_expression_quiet(state, context, expr_id)?; - unification::subtype_with_mode( - state, - context, - inferred, - expected, - ElaborationMode::No, - )?; + unification::subtype_with_mode(state, context, inferred, expected, ElaborationMode::No)?; crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) }) @@ -1820,7 +1814,13 @@ pub fn check_function_term_application( where Q: ExternalQueries, { - check_function_application_core(state, context, function_t, expression_id, check_expression_argument) + check_function_application_core( + state, + context, + function_t, + expression_id, + check_expression_argument, + ) } fn check_let_chunks( From 11392835ee6a7d8b8a6df22a85ac6a309785e178 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:00:50 +0800 Subject: [PATCH 062/386] Implement initial skeleton for exhaustiveness --- compiler-core/checking/src/algorithm.rs | 2 + .../checking/src/algorithm/exhaustiveness.rs | 219 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 compiler-core/checking/src/algorithm/exhaustiveness.rs diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 351784b4..41e175c7 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -15,6 +15,8 @@ pub mod constraint; /// Implements type class deriving. pub mod derive; +pub mod exhaustiveness; + /// Implements type folding for traversals that modify. pub mod fold; diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs new file mode 100644 index 00000000..b0ec3acb --- /dev/null +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -0,0 +1,219 @@ +use std::sync::Arc; + +use files::FileId; +use indexing::TermItemId; +use lowering::BinderId; +use rustc_hash::FxHashMap; +use smol_str::SmolStr; + +use crate::{ + ExternalQueries, TypeId, + algorithm::state::{CheckContext, CheckState}, +}; + +const MISSING_NAME: SmolStr = SmolStr::new_inline(""); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Pattern { + Wildcard, + Boolean(bool), + Char(char), + String(SmolStr), + Integer(i32), + Number(SmolStr), + Array { elements: Vec }, + Record { elements: Vec }, + Constructor { file_id: FileId, item_id: TermItemId, fields: Vec }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum RecordElement { + Named(SmolStr, PatternId), + Pun(SmolStr), +} + +pub type PatternId = interner::Id; + +type PatternVector = Vec; +type PatternMatrix = Vec; +type WitnessVector = Vec; + +struct ExhaustivenessState { + interner: interner::Interner, + types: FxHashMap, +} + +impl ExhaustivenessState { + fn allocate(&mut self, pattern: Pattern, t: TypeId) -> PatternId { + let id = self.interner.intern(pattern); + self.types.insert(id, t); + id + } + + fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { + self.allocate(Pattern::Wildcard, t) + } +} + +fn lower_binder_default( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + id: Option, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + if let Some(id) = id { + lower_binder(check_state, exhaustiveness_state, context, id) + } else { + exhaustiveness_state.allocate_wildcard(t) + } +} + +fn lower_binder( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + id: BinderId, +) -> PatternId +where + Q: ExternalQueries, +{ + let t = check_state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); + + let Some(kind) = context.lowered.info.get_binder_kind(id) else { + return exhaustiveness_state.allocate_wildcard(t); + }; + + match kind { + lowering::BinderKind::Typed { binder, .. } => { + lower_binder_default(check_state, exhaustiveness_state, context, *binder, t) + } + lowering::BinderKind::OperatorChain { .. } => { + lower_operator_chain_binder(check_state, exhaustiveness_state, context, id, t) + } + lowering::BinderKind::Integer => exhaustiveness_state.allocate(Pattern::Integer(42), t), + lowering::BinderKind::Number => { + exhaustiveness_state.allocate(Pattern::Number(SmolStr::new_inline("42.0")), t) + } + lowering::BinderKind::Constructor { resolution, arguments } => { + lower_constructor_binder(check_state, exhaustiveness_state, context, resolution, arguments, t) + } + lowering::BinderKind::Variable { .. } => exhaustiveness_state.allocate_wildcard(t), + lowering::BinderKind::Named { binder, .. } => { + lower_binder_default(check_state, exhaustiveness_state, context, *binder, t) + } + lowering::BinderKind::Wildcard => exhaustiveness_state.allocate_wildcard(t), + lowering::BinderKind::String => { + let pattern = Pattern::String(SmolStr::new_static("")); + exhaustiveness_state.allocate(pattern, t) + } + lowering::BinderKind::Char => { + let pattern = Pattern::Char('\0'); + exhaustiveness_state.allocate(pattern, t) + } + lowering::BinderKind::Boolean { boolean } => { + exhaustiveness_state.allocate(Pattern::Boolean(*boolean), t) + } + lowering::BinderKind::Array { array } => { + lower_array_binder(check_state, exhaustiveness_state, context, array, t) + } + lowering::BinderKind::Record { record } => { + lower_record_binder(check_state, exhaustiveness_state, context, record, t) + } + lowering::BinderKind::Parenthesized { parenthesized } => { + lower_binder_default(check_state, exhaustiveness_state, context, *parenthesized, t) + } + } +} + +fn lower_array_binder( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + array: &[BinderId], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let elements = array + .iter() + .map(|element| lower_binder(check_state, exhaustiveness_state, context, *element)) + .collect(); + exhaustiveness_state.allocate(Pattern::Array { elements }, t) +} + +fn lower_record_binder( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + record: &[lowering::BinderRecordItem], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let elements = record + .iter() + .map(|element| lower_record_element(check_state, exhaustiveness_state, context, element)) + .collect(); + exhaustiveness_state.allocate(Pattern::Record { elements }, t) +} + +fn lower_record_element( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + element: &lowering::BinderRecordItem, +) -> RecordElement +where + Q: ExternalQueries, +{ + match element { + lowering::BinderRecordItem::RecordField { name, value } => { + let name = name.clone().unwrap_or(MISSING_NAME); + let value = if let Some(value) = value { + lower_binder(check_state, exhaustiveness_state, context, *value) + } else { + exhaustiveness_state.allocate_wildcard(context.prim.unknown) + }; + RecordElement::Named(name, value) + } + lowering::BinderRecordItem::RecordPun { name, .. } => { + let name = name.clone().unwrap_or(MISSING_NAME); + RecordElement::Pun(name) + } + } +} + +fn lower_constructor_binder( + _check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + _resolution: &Option<(FileId, TermItemId)>, + _arguments: &Arc<[BinderId]>, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + // TODO: Build Pattern::Constructor with resolved file_id, item_id, and lowered argument patterns + exhaustiveness_state.allocate_wildcard(t) +} + +fn lower_operator_chain_binder( + _check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + _id: BinderId, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + // TODO: Lookup context.bracketed.binders.get(&id) and traverse tree to build Pattern::Constructor + exhaustiveness_state.allocate_wildcard(t) +} From 4c4f7f7c6c90afa52acf896c875131f36de57e77 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:13:40 +0800 Subject: [PATCH 063/386] Implement lowering for binder values --- .../checking/src/algorithm/binder.rs | 8 +- .../checking/src/algorithm/exhaustiveness.rs | 87 ++++++++++--------- .../lowering/src/algorithm/recursive.rs | 67 +++++++++++++- compiler-core/lowering/src/intermediate.rs | 8 +- compiler-core/syntax/src/cst.rs | 23 +++++ 5 files changed, 139 insertions(+), 54 deletions(-) diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index 0ee5f9b3..1a3a0369 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -110,7 +110,7 @@ where Ok(inferred_type) } - lowering::BinderKind::Integer => { + lowering::BinderKind::Integer { .. } => { let inferred_type = context.prim.int; if let BinderMode::Check { expected_type, .. } = mode { @@ -120,7 +120,7 @@ where Ok(inferred_type) } - lowering::BinderKind::Number => { + lowering::BinderKind::Number { .. } => { let inferred_type = context.prim.number; if let BinderMode::Check { expected_type, .. } = mode { @@ -197,7 +197,7 @@ where BinderMode::Check { expected_type, .. } => Ok(expected_type), }, - lowering::BinderKind::String => { + lowering::BinderKind::String { .. } => { let inferred_type = context.prim.string; if let BinderMode::Check { expected_type, .. } = mode { @@ -207,7 +207,7 @@ where Ok(inferred_type) } - lowering::BinderKind::Char => { + lowering::BinderKind::Char { .. } => { let inferred_type = context.prim.char; if let BinderMode::Check { expected_type, .. } = mode { diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index b0ec3acb..759df9eb 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -20,7 +20,7 @@ pub enum Pattern { Char(char), String(SmolStr), Integer(i32), - Number(SmolStr), + Number(bool, SmolStr), Array { elements: Vec }, Record { elements: Vec }, Constructor { file_id: FileId, item_id: TermItemId, fields: Vec }, @@ -55,23 +55,6 @@ impl ExhaustivenessState { } } -fn lower_binder_default( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - id: Option, - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - if let Some(id) = id { - lower_binder(check_state, exhaustiveness_state, context, id) - } else { - exhaustiveness_state.allocate_wildcard(t) - } -} - fn lower_binder( check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, @@ -88,32 +71,51 @@ where }; match kind { - lowering::BinderKind::Typed { binder, .. } => { - lower_binder_default(check_state, exhaustiveness_state, context, *binder, t) - } + lowering::BinderKind::Typed { binder, .. } => match binder { + Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), + None => exhaustiveness_state.allocate_wildcard(t), + }, lowering::BinderKind::OperatorChain { .. } => { lower_operator_chain_binder(check_state, exhaustiveness_state, context, id, t) } - lowering::BinderKind::Integer => exhaustiveness_state.allocate(Pattern::Integer(42), t), - lowering::BinderKind::Number => { - exhaustiveness_state.allocate(Pattern::Number(SmolStr::new_inline("42.0")), t) - } - lowering::BinderKind::Constructor { resolution, arguments } => { - lower_constructor_binder(check_state, exhaustiveness_state, context, resolution, arguments, t) + lowering::BinderKind::Integer { value } => match value { + Some(v) => exhaustiveness_state.allocate(Pattern::Integer(*v), t), + None => exhaustiveness_state.allocate_wildcard(t), + }, + lowering::BinderKind::Number { negative, value } => { + if let Some(value) = value { + let pattern = Pattern::Number(*negative, SmolStr::clone(value)); + exhaustiveness_state.allocate(pattern, t) + } else { + exhaustiveness_state.allocate_wildcard(t) + } } + lowering::BinderKind::Constructor { resolution, arguments } => lower_constructor_binder( + check_state, + exhaustiveness_state, + context, + resolution, + arguments, + t, + ), lowering::BinderKind::Variable { .. } => exhaustiveness_state.allocate_wildcard(t), - lowering::BinderKind::Named { binder, .. } => { - lower_binder_default(check_state, exhaustiveness_state, context, *binder, t) - } + lowering::BinderKind::Named { binder, .. } => match binder { + Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), + None => exhaustiveness_state.allocate_wildcard(t), + }, lowering::BinderKind::Wildcard => exhaustiveness_state.allocate_wildcard(t), - lowering::BinderKind::String => { - let pattern = Pattern::String(SmolStr::new_static("")); - exhaustiveness_state.allocate(pattern, t) - } - lowering::BinderKind::Char => { - let pattern = Pattern::Char('\0'); - exhaustiveness_state.allocate(pattern, t) + lowering::BinderKind::String { value, .. } => { + if let Some(value) = value { + let pattern = Pattern::String(SmolStr::clone(value)); + exhaustiveness_state.allocate(pattern, t) + } else { + exhaustiveness_state.allocate_wildcard(t) + } } + lowering::BinderKind::Char { value } => match value { + Some(v) => exhaustiveness_state.allocate(Pattern::Char(*v), t), + None => exhaustiveness_state.allocate_wildcard(t), + }, lowering::BinderKind::Boolean { boolean } => { exhaustiveness_state.allocate(Pattern::Boolean(*boolean), t) } @@ -123,9 +125,10 @@ where lowering::BinderKind::Record { record } => { lower_record_binder(check_state, exhaustiveness_state, context, record, t) } - lowering::BinderKind::Parenthesized { parenthesized } => { - lower_binder_default(check_state, exhaustiveness_state, context, *parenthesized, t) - } + lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { + Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), + None => exhaustiveness_state.allocate_wildcard(t), + }, } } @@ -192,7 +195,7 @@ where fn lower_constructor_binder( _check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, + _context: &CheckContext, _resolution: &Option<(FileId, TermItemId)>, _arguments: &Arc<[BinderId]>, t: TypeId, @@ -207,7 +210,7 @@ where fn lower_operator_chain_binder( _check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, + _context: &CheckContext, _id: BinderId, t: TypeId, ) -> PatternId diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index ede1b49e..6cbc2289 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -45,8 +45,25 @@ fn lower_binder_kind( .collect(); BinderKind::OperatorChain { head, tail } } - cst::Binder::BinderInteger(_) => BinderKind::Integer, - cst::Binder::BinderNumber(_) => BinderKind::Number, + cst::Binder::BinderInteger(cst) => { + let value = cst.integer_token().and_then(|token| { + let text = token.text(); + let integer = if let Some(hex) = text.strip_prefix("0x") { + let clean = hex.replace_smolstr("_", ""); + i32::from_str_radix(&clean, 16).ok()? + } else { + let clean = text.replace_smolstr("_", ""); + clean.parse().ok()? + }; + if cst.minus_token().is_some() { Some(-integer) } else { Some(integer) } + }); + BinderKind::Integer { value } + } + cst::Binder::BinderNumber(cst) => { + let negative = cst.minus_token().is_some(); + let value = cst.number_token().map(|token| SmolStr::from(token.text())); + BinderKind::Number { negative, value } + } cst::Binder::BinderConstructor(cst) => { let resolution = cst.name().and_then(|cst| { let (qualifier, name) = lower_qualified_name(&cst, cst::QualifiedName::upper)?; @@ -77,8 +94,50 @@ fn lower_binder_kind( BinderKind::Named { named, binder } } cst::Binder::BinderWildcard(_) => BinderKind::Wildcard, - cst::Binder::BinderString(_) => BinderKind::String, - cst::Binder::BinderChar(_) => BinderKind::Char, + cst::Binder::BinderString(cst) => { + let (kind, value) = if let Some(token) = cst.string() { + let text = token.text(); + let value = text + .strip_prefix('"') + .and_then(|text| text.strip_suffix('"')) + .map(SmolStr::from); + (StringKind::String, value) + } else if let Some(token) = cst.raw_string() { + let text = token.text(); + let value = text + .strip_prefix("\"\"\"") + .and_then(|text| text.strip_suffix("\"\"\"")) + .map(SmolStr::from); + (StringKind::RawString, value) + } else { + (StringKind::String, None) + }; + BinderKind::String { kind, value } + } + cst::Binder::BinderChar(cst) => { + let value = cst.char_token().and_then(|token| { + let text = token.text(); + let inner = text.strip_prefix('\'')?.strip_suffix('\'')?; + if let Some(escaped) = inner.strip_prefix('\\') { + match escaped.chars().next()? { + 'n' => Some('\n'), + 'r' => Some('\r'), + 't' => Some('\t'), + '\\' => Some('\\'), + '\'' => Some('\''), + '0' => Some('\0'), + 'x' if escaped.len() >= 3 => { + let hex = &escaped[1..3]; + u8::from_str_radix(hex, 16).ok().map(|b| b as char) + } + _ => None, + } + } else { + inner.chars().next() + } + }); + BinderKind::Char { value } + } cst::Binder::BinderTrue(_) => BinderKind::Boolean { boolean: true }, cst::Binder::BinderFalse(_) => BinderKind::Boolean { boolean: false }, cst::Binder::BinderArray(cst) => { diff --git a/compiler-core/lowering/src/intermediate.rs b/compiler-core/lowering/src/intermediate.rs index 7e136d0c..15ceaff4 100644 --- a/compiler-core/lowering/src/intermediate.rs +++ b/compiler-core/lowering/src/intermediate.rs @@ -26,14 +26,14 @@ pub enum BinderRecordItem { pub enum BinderKind { Typed { binder: Option, type_: Option }, OperatorChain { head: Option, tail: Arc<[OperatorPair]> }, - Integer, - Number, + Integer { value: Option }, + Number { negative: bool, value: Option }, Constructor { resolution: Option<(FileId, TermItemId)>, arguments: Arc<[BinderId]> }, Variable { variable: Option }, Named { named: Option, binder: Option }, Wildcard, - String, - Char, + String { kind: StringKind, value: Option }, + Char { value: Option }, Boolean { boolean: bool }, Array { array: Arc<[BinderId]> }, Record { record: Arc<[BinderRecordItem]> }, diff --git a/compiler-core/syntax/src/cst.rs b/compiler-core/syntax/src/cst.rs index a7658851..69f62233 100644 --- a/compiler-core/syntax/src/cst.rs +++ b/compiler-core/syntax/src/cst.rs @@ -1169,3 +1169,26 @@ has_token!( | string() -> STRING | raw_string() -> RAW_STRING ); + +has_token!( + BinderInteger + | minus_token() -> MINUS + | integer_token() -> INTEGER +); + +has_token!( + BinderNumber + | minus_token() -> MINUS + | number_token() -> NUMBER +); + +has_token!( + BinderString + | string() -> STRING + | raw_string() -> RAW_STRING +); + +has_token!( + BinderChar + | char_token() -> CHAR +); From 6d41d1c18698d064e4691bac1bfc40c82d34c3a5 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:13:54 +0800 Subject: [PATCH 064/386] Fix and format files --- compiler-compatibility/src/compat.rs | 1 - compiler-compatibility/src/loader.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/src/compat.rs index 33adba13..5f82f81d 100644 --- a/compiler-compatibility/src/compat.rs +++ b/compiler-compatibility/src/compat.rs @@ -8,7 +8,6 @@ use files::{FileId, Files}; use line_index::LineIndex; use rowan::TextSize; -use tracing::field; use url::Url; use crate::loader; diff --git a/compiler-compatibility/src/loader.rs b/compiler-compatibility/src/loader.rs index 4fb34598..c59440b1 100644 --- a/compiler-compatibility/src/loader.rs +++ b/compiler-compatibility/src/loader.rs @@ -20,10 +20,10 @@ fn load_file(engine: &mut QueryEngine, files: &mut Files, path: &Path) -> Option let content = files.content(id); engine.set_content(id, content); - if let Ok((parsed, _)) = engine.parsed(id) { - if let Some(name) = parsed.module_name() { - engine.set_module_file(&name, id); - } + if let Ok((parsed, _)) = engine.parsed(id) + && let Some(name) = parsed.module_name() + { + engine.set_module_file(&name, id); } Some(id) From d16fc4b403698cfb8bc1d85a8ca58bf46da2df40 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:17:52 +0800 Subject: [PATCH 065/386] Initial implementation for lower_constructor_binder --- .../checking/src/algorithm/exhaustiveness.rs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 759df9eb..516cbe6b 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use files::FileId; use indexing::TermItemId; +use itertools::Itertools; use lowering::BinderId; use rustc_hash::FxHashMap; use smol_str::SmolStr; @@ -193,18 +194,27 @@ where } fn lower_constructor_binder( - _check_state: &mut CheckState, + check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - _resolution: &Option<(FileId, TermItemId)>, - _arguments: &Arc<[BinderId]>, + context: &CheckContext, + resolution: &Option<(FileId, TermItemId)>, + arguments: &Arc<[BinderId]>, t: TypeId, ) -> PatternId where Q: ExternalQueries, { - // TODO: Build Pattern::Constructor with resolved file_id, item_id, and lowered argument patterns - exhaustiveness_state.allocate_wildcard(t) + let Some((file_id, item_id)) = resolution else { + return exhaustiveness_state.allocate_wildcard(t); + }; + + let fields = arguments + .iter() + .map(|argument| lower_binder(check_state, exhaustiveness_state, context, *argument)) + .collect_vec(); + + let pattern = Pattern::Constructor { file_id: *file_id, item_id: *item_id, fields }; + exhaustiveness_state.allocate(pattern, t) } fn lower_operator_chain_binder( From 429e968f84af3f25121422c75574bcb43be133bb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:21:48 +0800 Subject: [PATCH 066/386] Initial implementation for lower_operator_tree --- .../checking/src/algorithm/exhaustiveness.rs | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 516cbe6b..cf7a9a5b 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -6,11 +6,10 @@ use itertools::Itertools; use lowering::BinderId; use rustc_hash::FxHashMap; use smol_str::SmolStr; +use sugar::OperatorTree; -use crate::{ - ExternalQueries, TypeId, - algorithm::state::{CheckContext, CheckState}, -}; +use crate::algorithm::state::{CheckContext, CheckState}; +use crate::{ExternalQueries, TypeId}; const MISSING_NAME: SmolStr = SmolStr::new_inline(""); @@ -218,15 +217,59 @@ where } fn lower_operator_chain_binder( - _check_state: &mut CheckState, + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + id: BinderId, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let Some(tree) = context.bracketed.binders.get(&id) else { + return exhaustiveness_state.allocate_wildcard(t); + }; + + let Ok(tree) = tree else { + return exhaustiveness_state.allocate_wildcard(t); + }; + + lower_operator_tree(check_state, exhaustiveness_state, context, tree, t) +} + +fn lower_operator_tree( + check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - _id: BinderId, + context: &CheckContext, + tree: &OperatorTree, t: TypeId, ) -> PatternId where Q: ExternalQueries, { - // TODO: Lookup context.bracketed.binders.get(&id) and traverse tree to build Pattern::Constructor - exhaustiveness_state.allocate_wildcard(t) + match tree { + OperatorTree::Leaf(None) => exhaustiveness_state.allocate_wildcard(context.prim.unknown), + OperatorTree::Leaf(Some(binder_id)) => { + lower_binder(check_state, exhaustiveness_state, context, *binder_id) + } + OperatorTree::Branch(operator_id, children) => { + let Some((file_id, item_id)) = context.lowered.info.get_term_operator(*operator_id) + else { + return exhaustiveness_state.allocate_wildcard(t); + }; + + let [left_tree, right_tree] = &**children; + + let left_tree = + lower_operator_tree(check_state, exhaustiveness_state, context, left_tree, t); + + let right_tree = + lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, t); + + let pattern = + Pattern::Constructor { file_id, item_id, fields: vec![left_tree, right_tree] }; + + exhaustiveness_state.allocate(pattern, t) + } + } } From 202837660b4f29178a0449a8397355f4a57bf9a0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 18:22:50 +0800 Subject: [PATCH 067/386] Format files --- compiler-compatibility/src/registry.rs | 6 ++++-- compiler-compatibility/src/resolver.rs | 6 ++---- compiler-compatibility/src/storage.rs | 13 +++++++------ compiler-compatibility/src/unpacker.rs | 6 ++---- compiler-scripts/src/test_runner/pending.rs | 6 ++---- purescript-registry/src/reader.rs | 11 +++++------ 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs index e3b681e7..4f332965 100644 --- a/compiler-compatibility/src/registry.rs +++ b/compiler-compatibility/src/registry.rs @@ -1,9 +1,11 @@ use std::fs; -use git2::{FetchOptions, Repository, build::RepoBuilder}; +use git2::build::RepoBuilder; +use git2::{FetchOptions, Repository}; use purescript_registry::FsRegistry; -use crate::{error::Result, layout::Layout}; +use crate::error::Result; +use crate::layout::Layout; const REGISTRY_URL: &str = "https://github.com/purescript/registry"; const REGISTRY_INDEX_URL: &str = "https://github.com/purescript/registry-index"; diff --git a/compiler-compatibility/src/resolver.rs b/compiler-compatibility/src/resolver.rs index 725a336b..cf931afb 100644 --- a/compiler-compatibility/src/resolver.rs +++ b/compiler-compatibility/src/resolver.rs @@ -3,10 +3,8 @@ use std::collections::{BTreeMap, HashSet}; use purescript_registry::{PackageSet, RegistryReader}; use semver::Version; -use crate::{ - error::{CompatError, Result}, - types::ResolvedSet, -}; +use crate::error::{CompatError, Result}; +use crate::types::ResolvedSet; pub fn resolve( root_packages: &[String], diff --git a/compiler-compatibility/src/storage.rs b/compiler-compatibility/src/storage.rs index 6c209d99..24a9df96 100644 --- a/compiler-compatibility/src/storage.rs +++ b/compiler-compatibility/src/storage.rs @@ -1,11 +1,11 @@ -use std::{fs, io::Read, path::PathBuf}; +use std::fs; +use std::io::Read; +use std::path::PathBuf; use sha2::{Digest, Sha256}; -use crate::{ - error::{CompatError, Result}, - layout::Layout, -}; +use crate::error::{CompatError, Result}; +use crate::layout::Layout; pub fn tarball_url(name: &str, version: &str) -> String { format!("https://packages.registry.purescript.org/{}/{}.tar.gz", name, version) @@ -42,7 +42,8 @@ pub fn verify_tarball( name: &str, version: &str, ) -> Result<()> { - use base64::{Engine, engine::general_purpose::STANDARD}; + use base64::Engine; + use base64::engine::general_purpose::STANDARD; let mut file = fs::File::open(path)?; let mut hasher = Sha256::new(); diff --git a/compiler-compatibility/src/unpacker.rs b/compiler-compatibility/src/unpacker.rs index 8b7d02fa..025b0bc2 100644 --- a/compiler-compatibility/src/unpacker.rs +++ b/compiler-compatibility/src/unpacker.rs @@ -1,7 +1,5 @@ -use std::{ - fs::{self, File}, - path::{Path, PathBuf}, -}; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; use flate2::read::GzDecoder; use tar::Archive; diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs index 846ec45d..aa69dfaa 100644 --- a/compiler-scripts/src/test_runner/pending.rs +++ b/compiler-scripts/src/test_runner/pending.rs @@ -1,16 +1,14 @@ -use std::env; -use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::{env, fs}; use console::style; use serde::Deserialize; use crate::test_runner::category::TestCategory; use crate::test_runner::cli::RunArgs; -use crate::test_runner::decision; use crate::test_runner::decision::DecisionInput; -use crate::test_runner::ui; +use crate::test_runner::{decision, ui}; #[derive(Deserialize)] struct PendingSnapshotJson { diff --git a/purescript-registry/src/reader.rs b/purescript-registry/src/reader.rs index a76c8ae9..7b1ca7d8 100644 --- a/purescript-registry/src/reader.rs +++ b/purescript-registry/src/reader.rs @@ -1,10 +1,9 @@ -use std::{fs, io::BufRead}; +use std::fs; +use std::io::BufRead; -use crate::{ - error::{RegistryError, Result}, - layout::RegistryLayout, - types::{Manifest, Metadata, PackageSet}, -}; +use crate::error::{RegistryError, Result}; +use crate::layout::RegistryLayout; +use crate::types::{Manifest, Metadata, PackageSet}; pub trait RegistryReader { fn list_package_sets(&self) -> Result>; From daff9263fdea44abf24749306463ecccacea2463 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 19:10:46 +0800 Subject: [PATCH 068/386] Initial scaffolding for pattern algorithms --- .../checking/src/algorithm/exhaustiveness.rs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index cf7a9a5b..ce02acdb 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,3 +1,5 @@ +#![allow(dead_code, unused_variables)] + use std::sync::Arc; use files::FileId; @@ -273,3 +275,74 @@ where } } } + +fn algorithm_u( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> bool +where + Q: ExternalQueries, +{ + todo!() +} + +fn algorithm_m( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> Option> +where + Q: ExternalQueries, +{ + todo!() +} + +fn specialise_matrix( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + (file_id, term_id): (FileId, TermItemId), + arity: usize, + matrix: &PatternMatrix, +) -> PatternMatrix +where + Q: ExternalQueries, +{ + todo!() +} + +fn specialise_vector( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + (file_id, term_id): (FileId, TermItemId), + arity: usize, + matrix: &PatternVector, +) -> PatternVector +where + Q: ExternalQueries, +{ + todo!() +} + +fn default_matrix( + exhaustiveness_state: &ExhaustivenessState, + matrix: &PatternMatrix, +) -> PatternMatrix { + let filter_map = matrix.iter().filter_map(|row| { + let [first_column, ref default_columns @ ..] = row[..] else { + return None; + }; + if let Pattern::Wildcard = &exhaustiveness_state.interner[first_column] { + Some(default_columns.to_vec()) + } else { + None + } + }); + filter_map.collect() +} From 06a37ecd152a2f59bfc682d0bf34ff135d30d8a6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 20:19:56 +0800 Subject: [PATCH 069/386] Record operator branch types during checking --- .../checking/src/algorithm/operator.rs | 50 ++++++++++++++++++- compiler-core/checking/src/algorithm/state.rs | 27 +++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking/src/algorithm/operator.rs b/compiler-core/checking/src/algorithm/operator.rs index 5e367b60..c79f9a40 100644 --- a/compiler-core/checking/src/algorithm/operator.rs +++ b/compiler-core/checking/src/algorithm/operator.rs @@ -6,7 +6,7 @@ use sugar::OperatorTree; use sugar::bracketing::BracketingResult; use crate::ExternalQueries; -use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; use crate::algorithm::{binder, kind, term, toolkit, unification}; use crate::core::{Type, TypeId}; @@ -78,6 +78,7 @@ where traverse_operator_branch( state, context, + *operator_id, (file_id, item_id), operator_type, children, @@ -90,6 +91,7 @@ where fn traverse_operator_branch( state: &mut CheckState, context: &CheckContext, + operator_id: E::OperatorId, operator: (FileId, E::ItemId), operator_type: TypeId, children: &[OperatorTree; 2], @@ -117,6 +119,8 @@ where return Ok(unknown); }; + E::record_branch_types(state, operator_id, left_type, right_type, result_type); + if let OperatorKindMode::Check { expected_type } = mode { let _ = unification::subtype(state, context, result_type, expected_type)?; } @@ -183,6 +187,14 @@ pub trait IsOperator: IsElement { result_tree: (Self::Elaborated, Self::Elaborated), result_type: TypeId, ) -> (Self::Elaborated, TypeId); + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ); } impl IsOperator for lowering::TypeId { @@ -247,6 +259,18 @@ impl IsOperator for lowering::TypeId { (elaborated_type, result_kind) } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .type_scope + .bind_operator_node(operator_id, OperatorBranchTypes { left, right, result }); + } } impl IsOperator for lowering::ExpressionId { @@ -306,6 +330,18 @@ impl IsOperator for lowering::ExpressionId { ) -> (Self::Elaborated, TypeId) { ((), result_type) } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .term_scope + .bind_operator_node(operator_id, OperatorBranchTypes { left, right, result }); + } } impl IsOperator for lowering::BinderId { @@ -365,4 +401,16 @@ impl IsOperator for lowering::BinderId { ) -> (Self::Elaborated, TypeId) { ((), result_type) } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .term_scope + .bind_operator_node(operator_id, OperatorBranchTypes { left, right, result }); + } } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 251370cc..943ea9bb 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -11,7 +11,7 @@ use files::FileId; use indexing::{IndexedModule, TermItemId, TypeItemId}; use lowering::{ BinderId, GraphNodeId, GroupedModule, ImplicitBindingId, LetBindingNameGroupId, LoweredModule, - RecordPunId, TypeItemIr, TypeVariableBindingId, + RecordPunId, TermOperatorId, TypeItemIr, TypeOperatorId, TypeVariableBindingId, }; use resolving::ResolvedModule; use rustc_hash::FxHashMap; @@ -24,11 +24,19 @@ use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; use crate::error::{CheckError, ErrorKind, ErrorStep}; use crate::{CheckedModule, ExternalQueries, TypeErrorMessageId}; +#[derive(Copy, Clone, Debug)] +pub struct OperatorBranchTypes { + pub left: TypeId, + pub right: TypeId, + pub result: TypeId, +} + /// Manually-managed scope for type-level bindings. #[derive(Default)] pub struct TypeScope { pub bound: debruijn::Bound, pub kinds: debruijn::BoundMap, + pub operator_node: FxHashMap, } impl TypeScope { @@ -112,6 +120,14 @@ impl TypeScope { pub fn size(&self) -> debruijn::Size { self.bound.size() } + + pub fn bind_operator_node(&mut self, id: TypeOperatorId, types: OperatorBranchTypes) { + self.operator_node.insert(id, types); + } + + pub fn lookup_operator_node(&self, id: TypeOperatorId) -> Option { + self.operator_node.get(&id).copied() + } } /// Manually-managed scope for term-level bindings. @@ -121,6 +137,7 @@ pub struct TermScope { pub let_binding: FxHashMap, pub record_pun: FxHashMap, pub section: FxHashMap, + pub operator_node: FxHashMap, } impl TermScope { @@ -155,6 +172,14 @@ impl TermScope { pub fn lookup_section(&self, id: lowering::ExpressionId) -> Option { self.section.get(&id).copied() } + + pub fn bind_operator_node(&mut self, id: TermOperatorId, types: OperatorBranchTypes) { + self.operator_node.insert(id, types); + } + + pub fn lookup_operator_node(&self, id: TermOperatorId) -> Option { + self.operator_node.get(&id).copied() + } } /// A single implicit variable captured from an instance head. From 56cfdc8f140a00deee7ace0b758a1e0c8ee5d091 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 20:25:04 +0800 Subject: [PATCH 070/386] Implement tree lowering with operator branch types --- .../checking/src/algorithm/exhaustiveness.rs | 104 +++++++++++------- 1 file changed, 64 insertions(+), 40 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index ce02acdb..65bccf61 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -5,12 +5,12 @@ use std::sync::Arc; use files::FileId; use indexing::TermItemId; use itertools::Itertools; -use lowering::BinderId; +use lowering::{BinderId, TermOperatorId}; use rustc_hash::FxHashMap; use smol_str::SmolStr; use sugar::OperatorTree; -use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; use crate::{ExternalQueries, TypeId}; const MISSING_NAME: SmolStr = SmolStr::new_inline(""); @@ -250,38 +250,62 @@ where Q: ExternalQueries, { match tree { - OperatorTree::Leaf(None) => exhaustiveness_state.allocate_wildcard(context.prim.unknown), + OperatorTree::Leaf(None) => exhaustiveness_state.allocate_wildcard(t), OperatorTree::Leaf(Some(binder_id)) => { lower_binder(check_state, exhaustiveness_state, context, *binder_id) } - OperatorTree::Branch(operator_id, children) => { - let Some((file_id, item_id)) = context.lowered.info.get_term_operator(*operator_id) - else { - return exhaustiveness_state.allocate_wildcard(t); - }; + OperatorTree::Branch(operator_id, children) => lower_operator_branch( + check_state, + exhaustiveness_state, + context, + *operator_id, + children, + t, + ), + } +} - let [left_tree, right_tree] = &**children; +fn lower_operator_branch( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + operator_id: TermOperatorId, + children: &[OperatorTree; 2], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { + return exhaustiveness_state.allocate_wildcard(t); + }; - let left_tree = - lower_operator_tree(check_state, exhaustiveness_state, context, left_tree, t); + let Some(OperatorBranchTypes { left, right, result }) = + check_state.term_scope.lookup_operator_node(operator_id) + else { + return exhaustiveness_state.allocate_wildcard(t); + }; - let right_tree = - lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, t); + let [left_tree, right_tree] = children; - let pattern = - Pattern::Constructor { file_id, item_id, fields: vec![left_tree, right_tree] }; + let left_pattern = + lower_operator_tree(check_state, exhaustiveness_state, context, left_tree, left); - exhaustiveness_state.allocate(pattern, t) - } - } + let right_pattern = + lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, right); + + let pattern = + Pattern::Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; + + exhaustiveness_state.allocate(pattern, result) } fn algorithm_u( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - matrix: &PatternMatrix, - vector: &PatternVector, + _check_state: &mut CheckState, + _exhaustiveness_state: &mut ExhaustivenessState, + _context: &CheckContext, + _matrix: &PatternMatrix, + _vector: &PatternVector, ) -> bool where Q: ExternalQueries, @@ -290,11 +314,11 @@ where } fn algorithm_m( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - matrix: &PatternMatrix, - vector: &PatternVector, + _check_state: &mut CheckState, + _exhaustiveness_state: &mut ExhaustivenessState, + _context: &CheckContext, + _matrix: &PatternMatrix, + _vector: &PatternVector, ) -> Option> where Q: ExternalQueries, @@ -303,12 +327,12 @@ where } fn specialise_matrix( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - (file_id, term_id): (FileId, TermItemId), - arity: usize, - matrix: &PatternMatrix, + _check_state: &mut CheckState, + _exhaustiveness_state: &mut ExhaustivenessState, + _context: &CheckContext, + (_file_id, _term_id): (FileId, TermItemId), + _arity: usize, + _matrix: &PatternMatrix, ) -> PatternMatrix where Q: ExternalQueries, @@ -317,12 +341,12 @@ where } fn specialise_vector( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - (file_id, term_id): (FileId, TermItemId), - arity: usize, - matrix: &PatternVector, + _check_state: &mut CheckState, + _exhaustiveness_state: &mut ExhaustivenessState, + _context: &CheckContext, + (_file_id, _term_id): (FileId, TermItemId), + _arity: usize, + _matrix: &PatternVector, ) -> PatternVector where Q: ExternalQueries, From 313156e34b17137ccc60582df2c5f8a15641c3e7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 30 Jan 2026 21:02:29 +0800 Subject: [PATCH 071/386] Initial implementation for specialisation --- .../checking/src/algorithm/exhaustiveness.rs | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 65bccf61..55fcc7fe 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,5 +1,6 @@ #![allow(dead_code, unused_variables)] +use std::iter; use std::sync::Arc; use files::FileId; @@ -25,7 +26,14 @@ pub enum Pattern { Number(bool, SmolStr), Array { elements: Vec }, Record { elements: Vec }, - Constructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Constructor { constructor: Constructor }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Constructor { + file_id: FileId, + item_id: TermItemId, + fields: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -214,7 +222,9 @@ where .map(|argument| lower_binder(check_state, exhaustiveness_state, context, *argument)) .collect_vec(); - let pattern = Pattern::Constructor { file_id: *file_id, item_id: *item_id, fields }; + let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; + let pattern = Pattern::Constructor { constructor }; + exhaustiveness_state.allocate(pattern, t) } @@ -294,8 +304,8 @@ where let right_pattern = lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, right); - let pattern = - Pattern::Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; + let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; + let pattern = Pattern::Constructor { constructor }; exhaustiveness_state.allocate(pattern, result) } @@ -327,31 +337,56 @@ where } fn specialise_matrix( - _check_state: &mut CheckState, - _exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - (_file_id, _term_id): (FileId, TermItemId), - _arity: usize, - _matrix: &PatternMatrix, + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + expected: Constructor, + matrix: &PatternMatrix, ) -> PatternMatrix where Q: ExternalQueries, { - todo!() + let matrix = matrix.iter().filter_map(|row| { + specialise_vector(check_state, exhaustiveness_state, context, &expected, row) + }); + matrix.collect() } fn specialise_vector( _check_state: &mut CheckState, - _exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - (_file_id, _term_id): (FileId, TermItemId), - _arity: usize, - _matrix: &PatternVector, -) -> PatternVector + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + expected: &Constructor, + vector: &PatternVector, +) -> Option where Q: ExternalQueries, { - todo!() + let [first_column, ref tail_columns @ ..] = vector[..] else { + unreachable!("invariant violated: specialise_vector processed empty row"); + }; + + let first_column = &exhaustiveness_state.interner[first_column]; + + if let Pattern::Wildcard = first_column { + let wildcards = expected.fields.iter().map(|&field_pat| { + let t = exhaustiveness_state.types.get(&field_pat); + let t = t.copied().unwrap_or(context.prim.unknown); + exhaustiveness_state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } + + let Pattern::Constructor { constructor } = first_column else { + return Some(tail_columns.to_vec()); + }; + + if (constructor.file_id, constructor.item_id) != (expected.file_id, expected.item_id) { + return None; + } + + Some(iter::chain(&constructor.fields, tail_columns).copied().collect()) } fn default_matrix( @@ -360,7 +395,7 @@ fn default_matrix( ) -> PatternMatrix { let filter_map = matrix.iter().filter_map(|row| { let [first_column, ref default_columns @ ..] = row[..] else { - return None; + unreachable!("invariant violated: default_matrix processed empty row"); }; if let Pattern::Wildcard = &exhaustiveness_state.interner[first_column] { Some(default_columns.to_vec()) From d4793e392b73ad506d3882c61faf2310d320e401 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 14:39:01 +0800 Subject: [PATCH 072/386] Refactor pattern to include types inline --- .../checking/src/algorithm/exhaustiveness.rs | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 55fcc7fe..24187cb4 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -7,7 +7,6 @@ use files::FileId; use indexing::TermItemId; use itertools::Itertools; use lowering::{BinderId, TermOperatorId}; -use rustc_hash::FxHashMap; use smol_str::SmolStr; use sugar::OperatorTree; @@ -17,7 +16,13 @@ use crate::{ExternalQueries, TypeId}; const MISSING_NAME: SmolStr = SmolStr::new_inline(""); #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum Pattern { +pub struct Pattern { + kind: PatternKind, + t: TypeId, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PatternKind { Wildcard, Boolean(bool), Char(char), @@ -50,18 +55,16 @@ type WitnessVector = Vec; struct ExhaustivenessState { interner: interner::Interner, - types: FxHashMap, } impl ExhaustivenessState { - fn allocate(&mut self, pattern: Pattern, t: TypeId) -> PatternId { - let id = self.interner.intern(pattern); - self.types.insert(id, t); - id + fn allocate(&mut self, kind: PatternKind, t: TypeId) -> PatternId { + let pattern = Pattern { kind, t }; + self.interner.intern(pattern) } fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { - self.allocate(Pattern::Wildcard, t) + self.allocate(PatternKind::Wildcard, t) } } @@ -89,13 +92,13 @@ where lower_operator_chain_binder(check_state, exhaustiveness_state, context, id, t) } lowering::BinderKind::Integer { value } => match value { - Some(v) => exhaustiveness_state.allocate(Pattern::Integer(*v), t), + Some(v) => exhaustiveness_state.allocate(PatternKind::Integer(*v), t), None => exhaustiveness_state.allocate_wildcard(t), }, lowering::BinderKind::Number { negative, value } => { if let Some(value) = value { - let pattern = Pattern::Number(*negative, SmolStr::clone(value)); - exhaustiveness_state.allocate(pattern, t) + let kind = PatternKind::Number(*negative, SmolStr::clone(value)); + exhaustiveness_state.allocate(kind, t) } else { exhaustiveness_state.allocate_wildcard(t) } @@ -116,18 +119,18 @@ where lowering::BinderKind::Wildcard => exhaustiveness_state.allocate_wildcard(t), lowering::BinderKind::String { value, .. } => { if let Some(value) = value { - let pattern = Pattern::String(SmolStr::clone(value)); - exhaustiveness_state.allocate(pattern, t) + let kind = PatternKind::String(SmolStr::clone(value)); + exhaustiveness_state.allocate(kind, t) } else { exhaustiveness_state.allocate_wildcard(t) } } lowering::BinderKind::Char { value } => match value { - Some(v) => exhaustiveness_state.allocate(Pattern::Char(*v), t), + Some(v) => exhaustiveness_state.allocate(PatternKind::Char(*v), t), None => exhaustiveness_state.allocate_wildcard(t), }, lowering::BinderKind::Boolean { boolean } => { - exhaustiveness_state.allocate(Pattern::Boolean(*boolean), t) + exhaustiveness_state.allocate(PatternKind::Boolean(*boolean), t) } lowering::BinderKind::Array { array } => { lower_array_binder(check_state, exhaustiveness_state, context, array, t) @@ -156,7 +159,7 @@ where .iter() .map(|element| lower_binder(check_state, exhaustiveness_state, context, *element)) .collect(); - exhaustiveness_state.allocate(Pattern::Array { elements }, t) + exhaustiveness_state.allocate(PatternKind::Array { elements }, t) } fn lower_record_binder( @@ -173,7 +176,7 @@ where .iter() .map(|element| lower_record_element(check_state, exhaustiveness_state, context, element)) .collect(); - exhaustiveness_state.allocate(Pattern::Record { elements }, t) + exhaustiveness_state.allocate(PatternKind::Record { elements }, t) } fn lower_record_element( @@ -223,9 +226,7 @@ where .collect_vec(); let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; - let pattern = Pattern::Constructor { constructor }; - - exhaustiveness_state.allocate(pattern, t) + exhaustiveness_state.allocate(PatternKind::Constructor { constructor }, t) } fn lower_operator_chain_binder( @@ -305,9 +306,7 @@ where lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, right); let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; - let pattern = Pattern::Constructor { constructor }; - - exhaustiveness_state.allocate(pattern, result) + exhaustiveness_state.allocate(PatternKind::Constructor { constructor }, result) } fn algorithm_u( @@ -368,17 +367,16 @@ where let first_column = &exhaustiveness_state.interner[first_column]; - if let Pattern::Wildcard = first_column { - let wildcards = expected.fields.iter().map(|&field_pat| { - let t = exhaustiveness_state.types.get(&field_pat); - let t = t.copied().unwrap_or(context.prim.unknown); + if let PatternKind::Wildcard = first_column.kind { + let wildcards = expected.fields.iter().map(|&pattern| { + let t = exhaustiveness_state.interner[pattern].t; exhaustiveness_state.allocate_wildcard(t) }); let tail_columns = tail_columns.iter().copied(); return Some(iter::chain(wildcards, tail_columns).collect()); } - let Pattern::Constructor { constructor } = first_column else { + let PatternKind::Constructor { constructor } = &first_column.kind else { return Some(tail_columns.to_vec()); }; @@ -397,7 +395,7 @@ fn default_matrix( let [first_column, ref default_columns @ ..] = row[..] else { unreachable!("invariant violated: default_matrix processed empty row"); }; - if let Pattern::Wildcard = &exhaustiveness_state.interner[first_column] { + if let PatternKind::Wildcard = exhaustiveness_state.interner[first_column].kind { Some(default_columns.to_vec()) } else { None From 84d3ba977803d8223ae2b48862c77c3c7dae91e4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 05:17:10 +0800 Subject: [PATCH 073/386] Implement initial algorithm_u and algorithm_m --- .../checking/src/algorithm/exhaustiveness.rs | 586 +++++++++++++++++- 1 file changed, 568 insertions(+), 18 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 24187cb4..07a4d8c9 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,12 +1,12 @@ -#![allow(dead_code, unused_variables)] - use std::iter; use std::sync::Arc; +use building_types::QueryResult; use files::FileId; use indexing::TermItemId; use itertools::Itertools; use lowering::{BinderId, TermOperatorId}; +use rustc_hash::FxHashSet; use smol_str::SmolStr; use sugar::OperatorTree; @@ -309,48 +309,475 @@ where exhaustiveness_state.allocate(PatternKind::Constructor { constructor }, result) } +/// Determines if a [`PatternVector`] is useful with respect to a [`PatternMatrix`]. +/// +/// A pattern vector is useful if it matches at least one value not matched by +/// any pattern vector in the matrix. This is the core algorithm from Maranget's +/// "Warnings for pattern matching" paper. fn algorithm_u( - _check_state: &mut CheckState, - _exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - _matrix: &PatternMatrix, - _vector: &PatternVector, -) -> bool + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult where Q: ExternalQueries, { - todo!() + // Base case: any pattern is useful against an empty matrix + if matrix.is_empty() { + return Ok(true); + } + + // Base case: an empty pattern vector against non-empty matrix is useless + let [first_pattern, ..] = vector[..] else { + return Ok(false); + }; + + let first_pattern = exhaustiveness_state.interner[first_pattern].clone(); + + match first_pattern.kind { + PatternKind::Constructor { constructor } => algorithm_u_constructor( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + constructor, + ), + PatternKind::Wildcard => { + algorithm_u_wildcard(check_state, exhaustiveness_state, context, matrix, vector) + } + _ => algorithm_u_other(check_state, exhaustiveness_state, context, matrix, vector), + } } +fn algorithm_u_constructor( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + constructor: Constructor, +) -> QueryResult +where + Q: ExternalQueries, +{ + let specialized_matrix = + specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + + let Some(specialized_vector) = + specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) + else { + unreachable!("invariant violated: vector contains constructor"); + }; + + algorithm_u( + check_state, + exhaustiveness_state, + context, + &specialized_matrix, + &specialized_vector, + ) +} + +fn algorithm_u_wildcard( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult +where + Q: ExternalQueries, +{ + let sigma = extract_sigma(exhaustiveness_state, matrix); + let complete = sigma_is_complete(context, &sigma)?; + + if complete { + // If sigma is complete, check if useful for any constructor + for constructor in sigma { + let specialized_matrix = + specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + let specialized_vector = + specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) + .expect("specialising wildcard head must succeed"); + + if algorithm_u( + check_state, + exhaustiveness_state, + context, + &specialized_matrix, + &specialized_vector, + )? { + return Ok(true); + } + } + Ok(false) + } else { + // If sigma is incomplete, use default matrix + let default = default_matrix(exhaustiveness_state, matrix); + let tail = vector[1..].to_vec(); + algorithm_u(check_state, exhaustiveness_state, context, &default, &tail) + } +} + +fn algorithm_u_other( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult +where + Q: ExternalQueries, +{ + // For literals and other patterns, treat as incomplete sigma (conservative) + let default = default_matrix(exhaustiveness_state, matrix); + let tail = vector[1..].to_vec(); + algorithm_u(check_state, exhaustiveness_state, context, &default, &tail) +} + +/// Determines the matching [`WitnessVector`] given a [`PatternMatrix`] +/// and some [`PatternVector`]. +/// +/// If the pattern vector is useful against the provided matrix, that is, +/// there are cases yet to be covered, this function will return a non-empty +/// list of witnesses. Inversely, if the pattern vector is useless against +/// the provided matrix, that is, the cases are exhaustive, this function +/// will return [`None`]. +/// +/// So... what exactly are witnesses? In the paper, these are defined as +/// 'value vectors' that are known not to be matched against the pattern +/// matrix but are instantiations of the pattern vector. In our implementation, +/// these witnesses are patterns not covered yet by the matrix. +/// +/// The [`algorithm_m_wildcard`] induction is prolific for producing these +/// these witnesses as it compares the constructors that appear in the +/// matrix against the constructors available in the checking environment. fn algorithm_m( - _check_state: &mut CheckState, - _exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, - _matrix: &PatternMatrix, - _vector: &PatternVector, -) -> Option> + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + // Base case: any pattern is its own witness against an empty matrix + if matrix.is_empty() { + let vector = vector.clone(); + return Ok(Some(vec![vector])); + } + + // Base case: an empty pattern vector against non-empty matrix has no witnesses + let [first_pattern, ..] = vector[..] else { + return Ok(None); + }; + + let first_pattern = exhaustiveness_state.interner[first_pattern].clone(); + + match first_pattern.kind { + PatternKind::Constructor { constructor } => algorithm_m_constructor( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + constructor, + first_pattern.t, + ), + PatternKind::Wildcard => algorithm_m_wildcard( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + first_pattern.t, + ), + _ => algorithm_m_other( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + first_pattern.t, + ), + } +} + +/// Induction 1 +/// +/// This function uses specialisation to spread the provided [`Constructor`] +/// over both the [`PatternMatrix`] and the [`PatternVector`], before calling +/// [`algorithm_m`] recursively with the specialised structures. +/// +/// The final set of witnesses returned by this induction includes a +/// reconstruction of the original constructor passed to this function. +/// +/// See documentation for [`specialise_matrix`] and [`specialise_vector`] for +/// more information on what specialisation entails given a constructor. +fn algorithm_m_constructor( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + constructor: Constructor, + first_type: TypeId, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let arity = constructor.fields.len(); + + let specialized_matrix = + specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + + let Some(specialized_vector) = + specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) + else { + unreachable!("invariant violated: vector contains constructor"); + }; + + let witnesses = algorithm_m( + check_state, + exhaustiveness_state, + context, + &specialized_matrix, + &specialized_vector, + )?; + + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + let witnesses = witnesses.into_iter().map(|witness| { + let (argument_columns, tail_columns) = witness.split_at(arity); + + let pattern = PatternKind::Constructor { + constructor: Constructor { + file_id: constructor.file_id, + item_id: constructor.item_id, + fields: argument_columns.to_vec(), + }, + }; + + let constructor = exhaustiveness_state.allocate(pattern, first_type); + let tail_columns = tail_columns.iter().copied(); + + iter::once(constructor).chain(tail_columns).collect() + }); + + let witnesses = witnesses.collect(); + Ok(Some(witnesses)) +} + +/// Induction 2 +/// +/// If the first column in the [`PatternVector`] is a wildcard, this function +/// produces witnesses that correspond to patterns not yet covered by the +/// [`PatternMatrix`]. This is where pattern suggestion warnings are built +/// for the compiler! +/// +/// This function collects all constructor references from the first column +/// of all rows in the matrix into a collection called the sigma. We handle +/// the structure in different ways: +/// +/// If the sigma is complete, for each constructor in the sigma, we apply +/// a rule similar to [`algorithm_m_constructor`] to collect witnesses +/// across all constructors. +/// +/// If the sigma is incomplete, we recursively apply [`algorithm_m`] to the +/// [`default_matrix`] of the pattern matrix and the tail columns of the +/// pattern vector. The induction ends if the recursive call is exhaustive. +/// +/// If the recursive call returns witnesses, and the sigma is non-empty, +/// we move our attention to generating [`Constructor`] patterns for +/// constructors not present in the sigma. This is what we use for +/// reporting pattern warnings. Otherwise, if the sigma is empty, we +/// simply produce a wildcard pattern. +fn algorithm_m_wildcard( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + first_type: TypeId, +) -> QueryResult>> where Q: ExternalQueries, { - todo!() + let sigma = extract_sigma(exhaustiveness_state, matrix); + let complete = sigma_is_complete(context, &sigma)?; + + if complete { + algorithm_m_wildcard_complete( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + first_type, + sigma, + ) + } else { + algorithm_m_wildcard_incomplete( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + first_type, + &sigma, + ) + } } +fn algorithm_m_wildcard_complete( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + first_type: TypeId, + sigma: Vec, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let mut all_witnesses = vec![]; + + for constructor in sigma { + let arity = constructor.fields.len(); + + let specialized_matrix = + specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + + let Some(specialized_vector) = + specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) + else { + unreachable!("invariant violated: vector contains constructor"); + }; + + if let Some(witnesses) = algorithm_m( + check_state, + exhaustiveness_state, + context, + &specialized_matrix, + &specialized_vector, + )? { + for witness in witnesses { + let (argument_columns, tail_columns) = witness.split_at(arity); + + let pattern = PatternKind::Constructor { + constructor: Constructor { + file_id: constructor.file_id, + item_id: constructor.item_id, + fields: argument_columns.to_vec(), + }, + }; + + let constructor = exhaustiveness_state.allocate(pattern, first_type); + let tail_columns = tail_columns.iter().copied(); + + let witnesses = iter::once(constructor).chain(tail_columns).collect(); + all_witnesses.push(witnesses); + } + } + } + + if all_witnesses.is_empty() { Ok(None) } else { Ok(Some(all_witnesses)) } +} + +fn algorithm_m_wildcard_incomplete( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + first_type: TypeId, + sigma: &[Constructor], +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let default = default_matrix(exhaustiveness_state, matrix); + let tail_columns = vector[1..].to_vec(); + + let witnesses = + algorithm_m(check_state, exhaustiveness_state, context, &default, &tail_columns)?; + + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + let head = if let Some((file_id, item_id, arity)) = pick_missing_constructor(context, sigma)? { + // FIXME: Use unification variables or constructor argument types for these wildcards, + // rather than `prim.unknown`. Also consider returning multiple missing constructors + // where possible to improve witness quality. + let fields = (0..arity) + .map(|_| exhaustiveness_state.allocate_wildcard(context.prim.unknown)) + .collect_vec(); + + let constructor = Constructor { file_id, item_id, fields }; + let pattern = PatternKind::Constructor { constructor }; + + exhaustiveness_state.allocate(pattern, first_type) + } else { + exhaustiveness_state.allocate_wildcard(first_type) + }; + + Ok(Some( + witnesses.into_iter().map(|witness| iter::once(head).chain(witness).collect()).collect(), + )) +} + +fn algorithm_m_other( + check_state: &mut CheckState, + exhaustiveness_state: &mut ExhaustivenessState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + first_type: TypeId, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + // For literals and other patterns, treat as incomplete sigma (conservative) + let default = default_matrix(exhaustiveness_state, matrix); + let tail = vector[1..].to_vec(); + + let witnesses = algorithm_m(check_state, exhaustiveness_state, context, &default, &tail)?; + + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + // Prefix with wildcard + let head = exhaustiveness_state.allocate_wildcard(first_type); + Ok(Some(witnesses.into_iter().map(|w| iter::once(head).chain(w).collect()).collect())) +} + +/// Specialises a [`PatternMatrix`] given a [`Constructor`]. fn specialise_matrix( check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, context: &CheckContext, - expected: Constructor, + expected: &Constructor, matrix: &PatternMatrix, ) -> PatternMatrix where Q: ExternalQueries, { let matrix = matrix.iter().filter_map(|row| { - specialise_vector(check_state, exhaustiveness_state, context, &expected, row) + specialise_vector(check_state, exhaustiveness_state, context, expected, row) }); matrix.collect() } +/// Specialises a [`PatternVector`] given a [`Constructor`]. fn specialise_vector( _check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, @@ -403,3 +830,126 @@ fn default_matrix( }); filter_map.collect() } + +/// Key for identifying a unique constructor (file + term item). +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct ConstructorKey(FileId, TermItemId); + +/// Extracts the set of constructors (sigma) from the first column of the matrix. +/// +/// Returns a list of unique constructors seen in the first column, keeping one +/// representative `Constructor` per distinct constructor id. Non-constructor +/// patterns (wildcards, literals, etc.) are ignored. +fn extract_sigma( + exhaustiveness_state: &ExhaustivenessState, + matrix: &PatternMatrix, +) -> Vec { + let mut seen = FxHashSet::default(); + let mut sigma = vec![]; + + for row in matrix { + let [first_column, ..] = row[..] else { + continue; + }; + let pattern = &exhaustiveness_state.interner[first_column]; + if let PatternKind::Constructor { constructor } = &pattern.kind { + let key = ConstructorKey(constructor.file_id, constructor.item_id); + if seen.insert(key) { + sigma.push(Constructor::clone(constructor)); + } + } + } + + sigma +} + +/// Checks whether the set of constructors (sigma) is complete for the scrutinee type. +/// +/// A sigma is complete if it contains all constructors of the data type. If we can't +/// determine the type or its constructors, we conservatively return false (incomplete). +fn sigma_is_complete(context: &CheckContext, sigma: &[Constructor]) -> QueryResult +where + Q: ExternalQueries, +{ + // Empty sigma is never complete (we need at least one constructor to determine the type) + let Some(first) = sigma.first() else { + return Ok(false); + }; + + // Get the indexed module for the constructor's file + let indexed = context.queries.indexed(first.file_id)?; + + // Find the type this constructor belongs to + let Some(type_item_id) = indexed.pairs.constructor_type(first.item_id) else { + return Ok(false); + }; + + // Get all constructors for this type + let all_constructors: FxHashSet = + indexed.pairs.data_constructors(type_item_id).collect(); + + // Check if sigma covers all constructors + let sigma_terms: FxHashSet = sigma.iter().map(|c| c.item_id).collect(); + + Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) +} + +/// Picks a missing constructor not in sigma, for witness generation. +/// +/// Returns the term item id of a constructor not present in sigma, if one exists. +fn pick_missing_constructor( + context: &CheckContext, + sigma: &[Constructor], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(first) = sigma.first() else { + return Ok(None); + }; + + let indexed = context.queries.indexed(first.file_id)?; + + let Some(type_item_id) = indexed.pairs.constructor_type(first.item_id) else { + return Ok(None); + }; + + let sigma_terms: FxHashSet = sigma.iter().map(|c| c.item_id).collect(); + + for term_id in indexed.pairs.data_constructors(type_item_id) { + if !sigma_terms.contains(&term_id) { + // Get the arity of this constructor from its item info + let arity = get_constructor_arity(context, first.file_id, term_id)?; + return Ok(Some((first.file_id, term_id, arity))); + } + } + + Ok(None) +} + +/// Gets the arity (number of fields) of a constructor. +fn get_constructor_arity( + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let on_lowered = |lowered: &lowering::LoweredModule| { + if let Some(lowering::TermItemIr::Constructor { arguments }) = + lowered.info.get_term_item(term_id) + { + arguments.len() + } else { + 0 + } + }; + if file_id == context.id { + let lowered = &context.lowered; + Ok(on_lowered(lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(on_lowered(&lowered)) + } +} From 4539ad9f19c77296468abca7b7fc682321f47ebd Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 15:14:04 +0800 Subject: [PATCH 074/386] Create Sigma to track missing constructors --- .../checking/src/algorithm/exhaustiveness.rs | 150 +++++++++++++----- 1 file changed, 108 insertions(+), 42 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 07a4d8c9..829900bd 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -11,6 +11,7 @@ use smol_str::SmolStr; use sugar::OperatorTree; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; +use crate::algorithm::{derive, toolkit}; use crate::{ExternalQueries, TypeId}; const MISSING_NAME: SmolStr = SmolStr::new_inline(""); @@ -345,9 +346,14 @@ where vector, constructor, ), - PatternKind::Wildcard => { - algorithm_u_wildcard(check_state, exhaustiveness_state, context, matrix, vector) - } + PatternKind::Wildcard => algorithm_u_wildcard( + check_state, + exhaustiveness_state, + context, + matrix, + vector, + first_pattern.t, + ), _ => algorithm_u_other(check_state, exhaustiveness_state, context, matrix, vector), } } @@ -387,16 +393,17 @@ fn algorithm_u_wildcard( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, + first_type: TypeId, ) -> QueryResult where Q: ExternalQueries, { - let sigma = extract_sigma(exhaustiveness_state, matrix); + let sigma = collect_sigma(check_state, exhaustiveness_state, context, matrix, first_type)?; let complete = sigma_is_complete(context, &sigma)?; if complete { // If sigma is complete, check if useful for any constructor - for constructor in sigma { + for constructor in sigma.constructors { let specialized_matrix = specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); let specialized_vector = @@ -609,9 +616,11 @@ fn algorithm_m_wildcard( where Q: ExternalQueries, { - let sigma = extract_sigma(exhaustiveness_state, matrix); + let sigma = collect_sigma(check_state, exhaustiveness_state, context, matrix, first_type)?; let complete = sigma_is_complete(context, &sigma)?; + let Sigma { constructors, missing } = sigma; + if complete { algorithm_m_wildcard_complete( check_state, @@ -620,7 +629,7 @@ where matrix, vector, first_type, - sigma, + constructors, ) } else { algorithm_m_wildcard_incomplete( @@ -630,7 +639,7 @@ where matrix, vector, first_type, - &sigma, + &missing, ) } } @@ -698,7 +707,7 @@ fn algorithm_m_wildcard_incomplete( matrix: &PatternMatrix, vector: &PatternVector, first_type: TypeId, - sigma: &[Constructor], + missing: &[MissingConstructor], ) -> QueryResult>> where Q: ExternalQueries, @@ -713,15 +722,19 @@ where return Ok(None); }; - let head = if let Some((file_id, item_id, arity)) = pick_missing_constructor(context, sigma)? { - // FIXME: Use unification variables or constructor argument types for these wildcards, - // rather than `prim.unknown`. Also consider returning multiple missing constructors - // where possible to improve witness quality. - let fields = (0..arity) - .map(|_| exhaustiveness_state.allocate_wildcard(context.prim.unknown)) + let head = if let Some(missing_constructor) = missing.first() { + // Use constructor field types when available; fall back to `prim.unknown`. + let fields = missing_constructor + .fields + .iter() + .map(|&t| exhaustiveness_state.allocate_wildcard(t)) .collect_vec(); - let constructor = Constructor { file_id, item_id, fields }; + let constructor = Constructor { + file_id: missing_constructor.file_id, + item_id: missing_constructor.item_id, + fields, + }; let pattern = PatternKind::Constructor { constructor }; exhaustiveness_state.allocate(pattern, first_type) @@ -781,7 +794,7 @@ where fn specialise_vector( _check_state: &mut CheckState, exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, + _context: &CheckContext, expected: &Constructor, vector: &PatternVector, ) -> Option @@ -835,17 +848,36 @@ fn default_matrix( #[derive(Clone, Copy, PartialEq, Eq, Hash)] struct ConstructorKey(FileId, TermItemId); +#[derive(Clone, Debug)] +struct Sigma { + constructors: Vec, + missing: Vec, +} + +#[derive(Clone, Debug)] +struct MissingConstructor { + file_id: FileId, + item_id: TermItemId, + fields: Vec, +} + /// Extracts the set of constructors (sigma) from the first column of the matrix. /// /// Returns a list of unique constructors seen in the first column, keeping one /// representative `Constructor` per distinct constructor id. Non-constructor /// patterns (wildcards, literals, etc.) are ignored. -fn extract_sigma( +fn collect_sigma( + check_state: &mut CheckState, exhaustiveness_state: &ExhaustivenessState, + context: &CheckContext, matrix: &PatternMatrix, -) -> Vec { + scrutinee_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ let mut seen = FxHashSet::default(); - let mut sigma = vec![]; + let mut constructors = vec![]; for row in matrix { let [first_column, ..] = row[..] else { @@ -855,24 +887,26 @@ fn extract_sigma( if let PatternKind::Constructor { constructor } = &pattern.kind { let key = ConstructorKey(constructor.file_id, constructor.item_id); if seen.insert(key) { - sigma.push(Constructor::clone(constructor)); + constructors.push(Constructor::clone(constructor)); } } } - sigma + let missing = + collect_missing_constructors(check_state, context, scrutinee_type, &constructors)?; + Ok(Sigma { constructors, missing }) } /// Checks whether the set of constructors (sigma) is complete for the scrutinee type. /// /// A sigma is complete if it contains all constructors of the data type. If we can't /// determine the type or its constructors, we conservatively return false (incomplete). -fn sigma_is_complete(context: &CheckContext, sigma: &[Constructor]) -> QueryResult +fn sigma_is_complete(context: &CheckContext, sigma: &Sigma) -> QueryResult where Q: ExternalQueries, { // Empty sigma is never complete (we need at least one constructor to determine the type) - let Some(first) = sigma.first() else { + let Some(first) = sigma.constructors.first() else { return Ok(false); }; @@ -889,42 +923,74 @@ where indexed.pairs.data_constructors(type_item_id).collect(); // Check if sigma covers all constructors - let sigma_terms: FxHashSet = sigma.iter().map(|c| c.item_id).collect(); + let sigma_terms: FxHashSet = sigma.constructors.iter().map(|c| c.item_id).collect(); Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) } -/// Picks a missing constructor not in sigma, for witness generation. -/// -/// Returns the term item id of a constructor not present in sigma, if one exists. -fn pick_missing_constructor( +fn collect_missing_constructors( + check_state: &mut CheckState, context: &CheckContext, - sigma: &[Constructor], -) -> QueryResult> + scrutinee_type: TypeId, + constructors: &[Constructor], +) -> QueryResult> where Q: ExternalQueries, { - let Some(first) = sigma.first() else { - return Ok(None); + let Some(first_constructor) = constructors.first() else { + return Ok(vec![]); }; - let indexed = context.queries.indexed(first.file_id)?; + let indexed = context.queries.indexed(first_constructor.file_id)?; - let Some(type_item_id) = indexed.pairs.constructor_type(first.item_id) else { - return Ok(None); + let Some(type_item_id) = indexed.pairs.constructor_type(first_constructor.item_id) else { + return Ok(vec![]); }; - let sigma_terms: FxHashSet = sigma.iter().map(|c| c.item_id).collect(); + let sigma: FxHashSet = constructors.iter().map(|c| c.item_id).collect(); + let type_arguments = toolkit::extract_all_applications(check_state, scrutinee_type); + let mut missing = vec![]; for term_id in indexed.pairs.data_constructors(type_item_id) { - if !sigma_terms.contains(&term_id) { - // Get the arity of this constructor from its item info - let arity = get_constructor_arity(context, first.file_id, term_id)?; - return Ok(Some((first.file_id, term_id, arity))); + if !sigma.contains(&term_id) { + let fields = constructor_field_types( + check_state, + context, + first_constructor.file_id, + term_id, + &type_arguments, + )?; + missing.push(MissingConstructor { + file_id: first_constructor.file_id, + item_id: term_id, + fields, + }); } } - Ok(None) + Ok(missing) +} + +fn constructor_field_types( + check_state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, + type_arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let constructor_type = derive::lookup_local_term_type(check_state, context, file_id, term_id)?; + if let Some(constructor_type) = constructor_type { + let constructor = + toolkit::instantiate_with_arguments(check_state, constructor_type, type_arguments); + let (fields, _) = toolkit::extract_function_arguments(check_state, constructor); + Ok(fields) + } else { + let arity = get_constructor_arity(context, file_id, term_id)?; + Ok(iter::repeat(context.prim.unknown).take(arity).collect()) + } } /// Gets the arity (number of fields) of a constructor. From 2cc3bfc031a44bf916d832e1fbe0b3c0034620b3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 20:17:18 +0800 Subject: [PATCH 075/386] Initial check for exhaustiveness in case expressions --- .../checking/src/algorithm/exhaustiveness.rs | 197 ++++++++++++++++-- compiler-core/checking/src/algorithm/term.rs | 12 +- .../checking/255_exhaustive_basic/Main.purs | 9 + .../checking/255_exhaustive_basic/Main.snap | 34 +++ .../256_exhaustive_multiple/Main.purs | 29 +++ .../256_exhaustive_multiple/Main.snap | 47 +++++ .../checking/257_exhaustive_tuple/Main.purs | 31 +++ .../checking/257_exhaustive_tuple/Main.snap | 59 ++++++ tests-integration/tests/checking/generated.rs | 6 + 9 files changed, 403 insertions(+), 21 deletions(-) create mode 100644 tests-integration/fixtures/checking/255_exhaustive_basic/Main.purs create mode 100644 tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap create mode 100644 tests-integration/fixtures/checking/256_exhaustive_multiple/Main.purs create mode 100644 tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap create mode 100644 tests-integration/fixtures/checking/257_exhaustive_tuple/Main.purs create mode 100644 tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 829900bd..8ffd3f0c 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -54,6 +54,7 @@ type PatternVector = Vec; type PatternMatrix = Vec; type WitnessVector = Vec; +#[derive(Default)] struct ExhaustivenessState { interner: interner::Interner, } @@ -937,34 +938,26 @@ fn collect_missing_constructors( where Q: ExternalQueries, { - let Some(first_constructor) = constructors.first() else { + let Some(constructor) = constructors.first() else { return Ok(vec![]); }; - let indexed = context.queries.indexed(first_constructor.file_id)?; + let indexed = context.queries.indexed(constructor.file_id)?; - let Some(type_item_id) = indexed.pairs.constructor_type(first_constructor.item_id) else { + let Some(type_item_id) = indexed.pairs.constructor_type(constructor.item_id) else { return Ok(vec![]); }; let sigma: FxHashSet = constructors.iter().map(|c| c.item_id).collect(); - let type_arguments = toolkit::extract_all_applications(check_state, scrutinee_type); + let arguments = toolkit::extract_all_applications(check_state, scrutinee_type); let mut missing = vec![]; - for term_id in indexed.pairs.data_constructors(type_item_id) { - if !sigma.contains(&term_id) { - let fields = constructor_field_types( - check_state, - context, - first_constructor.file_id, - term_id, - &type_arguments, - )?; - missing.push(MissingConstructor { - file_id: first_constructor.file_id, - item_id: term_id, - fields, - }); + for item_id in indexed.pairs.data_constructors(type_item_id) { + let file_id = constructor.file_id; + if !sigma.contains(&item_id) { + let fields = + constructor_field_types(check_state, context, file_id, item_id, &arguments)?; + missing.push(MissingConstructor { file_id, item_id, fields }); } } @@ -976,7 +969,7 @@ fn constructor_field_types( context: &CheckContext, file_id: FileId, term_id: TermItemId, - type_arguments: &[TypeId], + arguments: &[TypeId], ) -> QueryResult> where Q: ExternalQueries, @@ -984,7 +977,7 @@ where let constructor_type = derive::lookup_local_term_type(check_state, context, file_id, term_id)?; if let Some(constructor_type) = constructor_type { let constructor = - toolkit::instantiate_with_arguments(check_state, constructor_type, type_arguments); + toolkit::instantiate_with_arguments(check_state, constructor_type, arguments); let (fields, _) = toolkit::extract_function_arguments(check_state, constructor); Ok(fields) } else { @@ -1019,3 +1012,167 @@ where Ok(on_lowered(&lowered)) } } + +/// Checks exhaustiveness of case branches against scrutinee types. +/// +/// Returns [`None`] if the case expression is exhaustive, otherwise returns +/// [`Some`] witnesses with the rendered missing patterns if there are uncovered +/// cases. Only unconditional branches are counted towards exhaustiveness, as +/// pattern guards do not guarantee coverage. +pub fn check_case_exhaustiveness( + check_state: &mut CheckState, + context: &CheckContext, + trunk_types: &[TypeId], + branches: &[lowering::CaseBranch], +) -> QueryResult>> +where + Q: ExternalQueries, +{ + if trunk_types.is_empty() { + return Ok(None); + } + + let mut exhaustiveness_state = ExhaustivenessState::default(); + + // Build pattern matrix from unconditional branches only. + let mut matrix: PatternMatrix = vec![]; + for branch in branches { + let is_unconditional = matches!( + &branch.guarded_expression, + Some(lowering::GuardedExpression::Unconditional { .. }) | None + ); + if !is_unconditional { + continue; + } + + let mut row: PatternVector = vec![]; + for &binder_id in branch.binders.iter() { + let pattern = lower_binder(check_state, &mut exhaustiveness_state, context, binder_id); + row.push(pattern); + } + + while row.len() < trunk_types.len() { + let wildcard = exhaustiveness_state.allocate_wildcard(trunk_types[row.len()]); + row.push(wildcard); + } + + if !row.is_empty() { + matrix.push(row); + } + } + + let query: PatternVector = + trunk_types.iter().map(|&t| exhaustiveness_state.allocate_wildcard(t)).collect(); + + let witnesses = algorithm_m(check_state, &mut exhaustiveness_state, context, &matrix, &query)?; + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + let rendered = witnesses + .iter() + .take(5) + .map(|witness| render_witness(context, &exhaustiveness_state, witness)) + .collect(); + + Ok(Some(rendered)) +} + +fn render_witness( + context: &CheckContext, + state: &ExhaustivenessState, + witness: &WitnessVector, +) -> String +where + Q: ExternalQueries, +{ + witness.iter().map(|&id| render_pattern(context, state, id)).join(", ") +} + +fn render_pattern( + context: &CheckContext, + state: &ExhaustivenessState, + id: PatternId, +) -> String +where + Q: ExternalQueries, +{ + let pattern = &state.interner[id]; + match &pattern.kind { + PatternKind::Wildcard => "_".to_string(), + PatternKind::Boolean(b) => b.to_string(), + PatternKind::Char(c) => format!("'{c}'"), + PatternKind::String(s) => format!("\"{s}\""), + PatternKind::Integer(i) => i.to_string(), + PatternKind::Number(negative, n) => { + if *negative { + format!("-{n}") + } else { + n.to_string() + } + } + PatternKind::Array { elements } => { + let mut elements = elements.iter().map(|&e| render_pattern(context, state, e)); + format!("[{}]", elements.join(", ")) + } + PatternKind::Record { elements } => { + let mut elements = elements.iter().map(|e| match e { + RecordElement::Named(name, pattern) => { + let pattern = render_pattern(context, state, *pattern); + format!("{name}: {pattern}") + } + RecordElement::Pun(name) => name.to_string(), + }); + format!("{{ {} }}", elements.join(", ")) + } + PatternKind::Constructor { constructor } => render_constructor(context, state, constructor), + } +} + +fn render_constructor( + context: &CheckContext, + exhaustiveness_state: &ExhaustivenessState, + constructor: &Constructor, +) -> String +where + Q: ExternalQueries, +{ + let name = lookup_constructor_name(context, constructor.file_id, constructor.item_id) + .unwrap_or_else(|| "".to_string()); + + if constructor.fields.is_empty() { + return name; + } + + let mut fields = constructor.fields.iter().map(|&id| { + let rendered = render_pattern(context, exhaustiveness_state, id); + let pattern = &exhaustiveness_state.interner[id]; + if let PatternKind::Constructor { constructor } = &pattern.kind + && !constructor.fields.is_empty() + { + format!("({rendered})") + } else { + rendered + } + }); + + format!("{} {}", name, fields.join(" ")) +} + +fn lookup_constructor_name( + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> Option +where + Q: ExternalQueries, +{ + let indexed = if file_id == context.id { + Arc::clone(&context.indexed) + } else { + context.queries.indexed(file_id).ok()? + }; + + let item = &indexed.items[term_id]; + item.name.as_ref().map(|name| name.to_string()) +} diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 45f00790..8bef5085 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -9,7 +9,7 @@ use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{ - binder, inspect, kind, operator, substitute, toolkit, transfer, unification, + binder, exhaustiveness, inspect, kind, operator, substitute, toolkit, transfer, unification, }; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::{ErrorKind, ErrorStep}; @@ -618,6 +618,16 @@ where } } + // Check exhaustiveness after binders have been checked (so types are known) + if let Some(missing) = + exhaustiveness::check_case_exhaustiveness(state, context, &trunk_types, branches)? + { + let patterns = missing.join(", "); + let msg = format!("Pattern match is not exhaustive. Missing: {patterns}"); + let message_id = state.intern_error_message(msg); + state.insert_error(ErrorKind::CustomWarning { message_id }); + } + Ok(inferred_type) } diff --git a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.purs b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.purs new file mode 100644 index 00000000..9aab358e --- /dev/null +++ b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Maybe a = Just a | Nothing + +test1 = case _ of + Just _ -> 1 + +test2 = case _ of + Nothing -> 2 diff --git a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap new file mode 100644 index 00000000..5a0be2c8 --- /dev/null +++ b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test1 :: forall (t5 :: Type). Maybe (t5 :: Type) -> Int +test2 :: forall (t10 :: Type). Maybe (t10 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing + --> 5:9..6:14 + | +5 | test1 = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ + --> 8:9..9:15 + | +8 | test2 = case _ of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.purs b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.purs new file mode 100644 index 00000000..bfe45529 --- /dev/null +++ b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.purs @@ -0,0 +1,29 @@ +module Main where + +data Maybe a = Just a | Nothing + +complete = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete1 = case _, _ of + Just _, Nothing -> 2 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete2 = case _, _ of + Just _, Just _ -> 1 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete3 = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Nothing -> 4 + +incomplete4 = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Just _ -> 3 diff --git a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap new file mode 100644 index 00000000..ce56198b --- /dev/null +++ b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap @@ -0,0 +1,47 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +complete :: forall (t6 :: Type) (t7 :: Type). Maybe (t6 :: Type) -> Maybe (t7 :: Type) -> Int +incomplete1 :: forall (t19 :: Type) (t20 :: Type). Maybe (t19 :: Type) -> Maybe (t20 :: Type) -> Int +incomplete2 :: forall (t30 :: Type) (t31 :: Type). Maybe (t30 :: Type) -> Maybe (t31 :: Type) -> Int +incomplete3 :: forall (t41 :: Type) (t42 :: Type). Maybe (t41 :: Type) -> Maybe (t42 :: Type) -> Int +incomplete4 :: forall (t52 :: Type) (t53 :: Type). Maybe (t52 :: Type) -> Maybe (t53 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Just _ + --> 11:15..14:24 + | +11 | incomplete1 = case _, _ of + | ^~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Nothing + --> 16:15..19:24 + | +16 | incomplete2 = case _, _ of + | ^~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Just _ + --> 21:15..24:24 + | +21 | incomplete3 = case _, _ of + | ^~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Nothing + --> 26:15..29:23 + | +26 | incomplete4 = case _, _ of + | ^~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.purs b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.purs new file mode 100644 index 00000000..9e80fa60 --- /dev/null +++ b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.purs @@ -0,0 +1,31 @@ +module Main where + +data Tuple a b = Tuple a b + +data Maybe a = Just a | Nothing + +complete = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete1 = case _ of + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete2 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete3 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing Nothing -> 4 + +incomplete4 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 diff --git a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap new file mode 100644 index 00000000..03b37868 --- /dev/null +++ b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap @@ -0,0 +1,59 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +complete :: + forall (t9 :: Type) (t10 :: Type). Tuple (Maybe (t9 :: Type)) (Maybe (t10 :: Type)) -> Int +incomplete1 :: + forall (t29 :: Type) (t30 :: Type). Tuple (Maybe (t29 :: Type)) (Maybe (t30 :: Type)) -> Int +incomplete2 :: + forall (t45 :: Type) (t46 :: Type). Tuple (Maybe (t45 :: Type)) (Maybe (t46 :: Type)) -> Int +incomplete3 :: + forall (t61 :: Type) (t62 :: Type). Tuple (Maybe (t61 :: Type)) (Maybe (t62 :: Type)) -> Int +incomplete4 :: + forall (t77 :: Type) (t78 :: Type). Tuple (Maybe (t77 :: Type)) (Maybe (t78 :: Type)) -> Int + +Types +Tuple :: Type -> Type -> Type +Maybe :: Type -> Type + +Data +Tuple + Quantified = :0 + Kind = :0 + +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Tuple = [Representational, Representational] +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple (Just _) (Just _) + --> 13:15..16:29 + | +13 | incomplete1 = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple (Just _) Nothing + --> 18:15..21:29 + | +18 | incomplete2 = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple Nothing (Just _) + --> 23:15..26:29 + | +23 | incomplete3 = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple Nothing Nothing + --> 28:15..31:30 + | +28 | incomplete4 = case _ of + | ^~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index d8d27747..78a5201d 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -523,3 +523,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_253_invalid_type_application_too_many_main() { run_test("253_invalid_type_application_too_many", "Main"); } #[rustfmt::skip] #[test] fn test_254_higher_rank_elaboration_main() { run_test("254_higher_rank_elaboration", "Main"); } + +#[rustfmt::skip] #[test] fn test_255_exhaustive_basic_main() { run_test("255_exhaustive_basic", "Main"); } + +#[rustfmt::skip] #[test] fn test_256_exhaustive_multiple_main() { run_test("256_exhaustive_multiple", "Main"); } + +#[rustfmt::skip] #[test] fn test_257_exhaustive_tuple_main() { run_test("257_exhaustive_tuple", "Main"); } From 3dff67c49afc4f17b32730887b4ae297c9abcee1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 22:15:59 +0800 Subject: [PATCH 076/386] Add redundant pattern check --- .../checking/src/algorithm/exhaustiveness.rs | 72 +++++++++++++------ compiler-core/checking/src/algorithm/term.rs | 14 ++-- .../checking/258_redundant_patterns/Main.purs | 25 +++++++ .../checking/258_redundant_patterns/Main.snap | 58 +++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 tests-integration/fixtures/checking/258_redundant_patterns/Main.purs create mode 100644 tests-integration/fixtures/checking/258_redundant_patterns/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 8ffd3f0c..1e6de34e 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1013,29 +1013,33 @@ where } } -/// Checks exhaustiveness of case branches against scrutinee types. +pub struct CasePatternReport { + pub missing: Option>, + pub redundant: Vec, +} + +/// Checks case branch usefulness and exhaustiveness against scrutinee types. /// -/// Returns [`None`] if the case expression is exhaustive, otherwise returns -/// [`Some`] witnesses with the rendered missing patterns if there are uncovered -/// cases. Only unconditional branches are counted towards exhaustiveness, as -/// pattern guards do not guarantee coverage. -pub fn check_case_exhaustiveness( +/// Returns a report of missing patterns (if any) and redundant branches. +/// Only unconditional branches are counted toward these checks, as pattern +/// guards do not guarantee coverage. +pub fn check_case_patterns( check_state: &mut CheckState, context: &CheckContext, trunk_types: &[TypeId], branches: &[lowering::CaseBranch], -) -> QueryResult>> +) -> QueryResult where Q: ExternalQueries, { if trunk_types.is_empty() { - return Ok(None); + return Ok(CasePatternReport { missing: None, redundant: vec![] }); } let mut exhaustiveness_state = ExhaustivenessState::default(); // Build pattern matrix from unconditional branches only. - let mut matrix: PatternMatrix = vec![]; + let mut rows: Vec = vec![]; for branch in branches { let is_unconditional = matches!( &branch.guarded_expression, @@ -1057,25 +1061,53 @@ where } if !row.is_empty() { - matrix.push(row); + rows.push(row); + } + } + + let mut redundant = vec![]; + let mut matrix: PatternMatrix = vec![]; + for row in &rows { + let useful = algorithm_u(check_state, &mut exhaustiveness_state, context, &matrix, row)?; + if useful { + matrix.push(row.clone()); + } else { + redundant.push(render_witness(context, &exhaustiveness_state, row)); } } let query: PatternVector = trunk_types.iter().map(|&t| exhaustiveness_state.allocate_wildcard(t)).collect(); - let witnesses = algorithm_m(check_state, &mut exhaustiveness_state, context, &matrix, &query)?; - let Some(witnesses) = witnesses else { - return Ok(None); - }; + let witnesses = algorithm_m(check_state, &mut exhaustiveness_state, context, &rows, &query)?; + let missing = witnesses.map(|witnesses| { + witnesses + .iter() + .take(5) + .map(|witness| render_witness(context, &exhaustiveness_state, witness)) + .collect() + }); - let rendered = witnesses - .iter() - .take(5) - .map(|witness| render_witness(context, &exhaustiveness_state, witness)) - .collect(); + Ok(CasePatternReport { missing, redundant }) +} - Ok(Some(rendered)) +/// Checks exhaustiveness of case branches against scrutinee types. +/// +/// Returns [`None`] if the case expression is exhaustive, otherwise returns +/// [`Some`] witnesses with the rendered missing patterns if there are uncovered +/// cases. Only unconditional branches are counted towards exhaustiveness, as +/// pattern guards do not guarantee coverage. +pub fn check_case_exhaustiveness( + check_state: &mut CheckState, + context: &CheckContext, + trunk_types: &[TypeId], + branches: &[lowering::CaseBranch], +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let report = check_case_patterns(check_state, context, trunk_types, branches)?; + Ok(report.missing) } fn render_witness( diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 8bef5085..2ddc6065 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -618,16 +618,22 @@ where } } - // Check exhaustiveness after binders have been checked (so types are known) - if let Some(missing) = - exhaustiveness::check_case_exhaustiveness(state, context, &trunk_types, branches)? - { + // Check exhaustiveness/usefulness after binders have been checked (so types are known) + let report = exhaustiveness::check_case_patterns(state, context, &trunk_types, branches)?; + + if let Some(missing) = report.missing { let patterns = missing.join(", "); let msg = format!("Pattern match is not exhaustive. Missing: {patterns}"); let message_id = state.intern_error_message(msg); state.insert_error(ErrorKind::CustomWarning { message_id }); } + for redundant in report.redundant { + let msg = format!("Pattern match has redundant branch: {redundant}"); + let message_id = state.intern_error_message(msg); + state.insert_error(ErrorKind::CustomWarning { message_id }); + } + Ok(inferred_type) } diff --git a/tests-integration/fixtures/checking/258_redundant_patterns/Main.purs b/tests-integration/fixtures/checking/258_redundant_patterns/Main.purs new file mode 100644 index 00000000..660482d1 --- /dev/null +++ b/tests-integration/fixtures/checking/258_redundant_patterns/Main.purs @@ -0,0 +1,25 @@ +module Main where + +data Unit = Unit + +unit = case _ of + Unit -> 1 + _ -> 2 + Unit -> 3 + +data YesNo = Yes | No + +yes = case _ of + Yes -> 1 + _ -> 2 + Yes -> 3 + +no = case _ of + Yes -> 1 + _ -> 2 + No -> 3 + +yesNo = case _ of + Yes -> 1 + No -> 2 + _ -> 3 diff --git a/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap new file mode 100644 index 00000000..195690a7 --- /dev/null +++ b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap @@ -0,0 +1,58 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +unit :: Unit -> Int +Yes :: YesNo +No :: YesNo +yes :: YesNo -> Int +no :: YesNo -> Int +yesNo :: YesNo -> Int + +Types +Unit :: Type +YesNo :: Type + +Data +Unit + Quantified = :0 + Kind = :0 + +YesNo + Quantified = :0 + Kind = :0 + + +Roles +Unit = [] +YesNo = [] + +Diagnostics +warning[CustomWarning]: Pattern match has redundant branch: _ + --> 5:8..8:12 + | +5 | unit = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match has redundant branch: Unit + --> 5:8..8:12 + | +5 | unit = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match has redundant branch: Yes + --> 12:7..15:11 + | +12 | yes = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match has redundant branch: No + --> 17:6..20:10 + | +17 | no = case _ of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match has redundant branch: _ + --> 22:9..25:9 + | +22 | yesNo = case _ of + | ^~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 78a5201d..14605c5f 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -529,3 +529,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_256_exhaustive_multiple_main() { run_test("256_exhaustive_multiple", "Main"); } #[rustfmt::skip] #[test] fn test_257_exhaustive_tuple_main() { run_test("257_exhaustive_tuple", "Main"); } + +#[rustfmt::skip] #[test] fn test_258_redundant_patterns_main() { run_test("258_redundant_patterns", "Main"); } From b9272b9c9082a2f7202675be5d4e200156d5d713 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 22:29:18 +0800 Subject: [PATCH 077/386] Organise render functions into submodule --- .../checking/src/algorithm/exhaustiveness.rs | 124 +----------------- .../src/algorithm/exhaustiveness/render.rs | 110 ++++++++++++++++ 2 files changed, 114 insertions(+), 120 deletions(-) create mode 100644 compiler-core/checking/src/algorithm/exhaustiveness/render.rs diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 1e6de34e..86aea4e4 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,3 +1,5 @@ +mod render; + use std::iter; use std::sync::Arc; @@ -1072,7 +1074,7 @@ where if useful { matrix.push(row.clone()); } else { - redundant.push(render_witness(context, &exhaustiveness_state, row)); + redundant.push(render::render_witness(context, &exhaustiveness_state, row)); } } @@ -1084,127 +1086,9 @@ where witnesses .iter() .take(5) - .map(|witness| render_witness(context, &exhaustiveness_state, witness)) + .map(|witness| render::render_witness(context, &exhaustiveness_state, witness)) .collect() }); Ok(CasePatternReport { missing, redundant }) } - -/// Checks exhaustiveness of case branches against scrutinee types. -/// -/// Returns [`None`] if the case expression is exhaustive, otherwise returns -/// [`Some`] witnesses with the rendered missing patterns if there are uncovered -/// cases. Only unconditional branches are counted towards exhaustiveness, as -/// pattern guards do not guarantee coverage. -pub fn check_case_exhaustiveness( - check_state: &mut CheckState, - context: &CheckContext, - trunk_types: &[TypeId], - branches: &[lowering::CaseBranch], -) -> QueryResult>> -where - Q: ExternalQueries, -{ - let report = check_case_patterns(check_state, context, trunk_types, branches)?; - Ok(report.missing) -} - -fn render_witness( - context: &CheckContext, - state: &ExhaustivenessState, - witness: &WitnessVector, -) -> String -where - Q: ExternalQueries, -{ - witness.iter().map(|&id| render_pattern(context, state, id)).join(", ") -} - -fn render_pattern( - context: &CheckContext, - state: &ExhaustivenessState, - id: PatternId, -) -> String -where - Q: ExternalQueries, -{ - let pattern = &state.interner[id]; - match &pattern.kind { - PatternKind::Wildcard => "_".to_string(), - PatternKind::Boolean(b) => b.to_string(), - PatternKind::Char(c) => format!("'{c}'"), - PatternKind::String(s) => format!("\"{s}\""), - PatternKind::Integer(i) => i.to_string(), - PatternKind::Number(negative, n) => { - if *negative { - format!("-{n}") - } else { - n.to_string() - } - } - PatternKind::Array { elements } => { - let mut elements = elements.iter().map(|&e| render_pattern(context, state, e)); - format!("[{}]", elements.join(", ")) - } - PatternKind::Record { elements } => { - let mut elements = elements.iter().map(|e| match e { - RecordElement::Named(name, pattern) => { - let pattern = render_pattern(context, state, *pattern); - format!("{name}: {pattern}") - } - RecordElement::Pun(name) => name.to_string(), - }); - format!("{{ {} }}", elements.join(", ")) - } - PatternKind::Constructor { constructor } => render_constructor(context, state, constructor), - } -} - -fn render_constructor( - context: &CheckContext, - exhaustiveness_state: &ExhaustivenessState, - constructor: &Constructor, -) -> String -where - Q: ExternalQueries, -{ - let name = lookup_constructor_name(context, constructor.file_id, constructor.item_id) - .unwrap_or_else(|| "".to_string()); - - if constructor.fields.is_empty() { - return name; - } - - let mut fields = constructor.fields.iter().map(|&id| { - let rendered = render_pattern(context, exhaustiveness_state, id); - let pattern = &exhaustiveness_state.interner[id]; - if let PatternKind::Constructor { constructor } = &pattern.kind - && !constructor.fields.is_empty() - { - format!("({rendered})") - } else { - rendered - } - }); - - format!("{} {}", name, fields.join(" ")) -} - -fn lookup_constructor_name( - context: &CheckContext, - file_id: FileId, - term_id: TermItemId, -) -> Option -where - Q: ExternalQueries, -{ - let indexed = if file_id == context.id { - Arc::clone(&context.indexed) - } else { - context.queries.indexed(file_id).ok()? - }; - - let item = &indexed.items[term_id]; - item.name.as_ref().map(|name| name.to_string()) -} diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/render.rs b/compiler-core/checking/src/algorithm/exhaustiveness/render.rs new file mode 100644 index 00000000..d0d79baa --- /dev/null +++ b/compiler-core/checking/src/algorithm/exhaustiveness/render.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use files::FileId; +use indexing::TermItemId; +use itertools::Itertools; + +use crate::ExternalQueries; +use crate::algorithm::exhaustiveness::{ + Constructor, ExhaustivenessState, PatternId, PatternKind, RecordElement, WitnessVector, +}; +use crate::algorithm::state::CheckContext; + +pub fn render_witness( + context: &CheckContext, + state: &ExhaustivenessState, + witness: &WitnessVector, +) -> String +where + Q: ExternalQueries, +{ + witness.iter().map(|&id| render_pattern(context, state, id)).join(", ") +} + +fn render_pattern( + context: &CheckContext, + state: &ExhaustivenessState, + id: PatternId, +) -> String +where + Q: ExternalQueries, +{ + let pattern = &state.interner[id]; + match &pattern.kind { + PatternKind::Wildcard => "_".to_string(), + PatternKind::Boolean(b) => b.to_string(), + PatternKind::Char(c) => format!("'{c}'"), + PatternKind::String(s) => format!("\"{s}\""), + PatternKind::Integer(i) => i.to_string(), + PatternKind::Number(negative, n) => { + if *negative { + format!("-{n}") + } else { + n.to_string() + } + } + PatternKind::Array { elements } => { + let mut elements = elements.iter().map(|&e| render_pattern(context, state, e)); + format!("[{}]", elements.join(", ")) + } + PatternKind::Record { elements } => { + let mut elements = elements.iter().map(|e| match e { + RecordElement::Named(name, pattern) => { + let pattern = render_pattern(context, state, *pattern); + format!("{name}: {pattern}") + } + RecordElement::Pun(name) => name.to_string(), + }); + format!("{{ {} }}", elements.join(", ")) + } + PatternKind::Constructor { constructor } => render_constructor(context, state, constructor), + } +} + +fn render_constructor( + context: &CheckContext, + exhaustiveness_state: &ExhaustivenessState, + constructor: &Constructor, +) -> String +where + Q: ExternalQueries, +{ + let name = lookup_constructor_name(context, constructor.file_id, constructor.item_id) + .unwrap_or_else(|| "".to_string()); + + if constructor.fields.is_empty() { + return name; + } + + let mut fields = constructor.fields.iter().map(|&id| { + let rendered = render_pattern(context, exhaustiveness_state, id); + let pattern = &exhaustiveness_state.interner[id]; + if let PatternKind::Constructor { constructor } = &pattern.kind + && !constructor.fields.is_empty() + { + format!("({rendered})") + } else { + rendered + } + }); + + format!("{} {}", name, fields.join(" ")) +} + +fn lookup_constructor_name( + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> Option +where + Q: ExternalQueries, +{ + let indexed = if file_id == context.id { + Arc::clone(&context.indexed) + } else { + context.queries.indexed(file_id).ok()? + }; + + let item = &indexed.items[term_id]; + item.name.as_ref().map(|name| name.to_string()) +} From 05ce35265b4c0646868c3c7e639afd3793c35425 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 22:45:01 +0800 Subject: [PATCH 078/386] Move storage to CheckState, rename render to pretty --- .../checking/src/algorithm/exhaustiveness.rs | 466 ++++++------------ .../exhaustiveness/{render.rs => pretty.rs} | 32 +- compiler-core/checking/src/algorithm/state.rs | 15 + 3 files changed, 185 insertions(+), 328 deletions(-) rename compiler-core/checking/src/algorithm/exhaustiveness/{render.rs => pretty.rs} (74%) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 86aea4e4..282e9ed1 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,4 +1,4 @@ -mod render; +mod pretty; use std::iter; use std::sync::Arc; @@ -20,8 +20,8 @@ const MISSING_NAME: SmolStr = SmolStr::new_inline(""); #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Pattern { - kind: PatternKind, - t: TypeId, + pub kind: PatternKind, + pub t: TypeId, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -39,9 +39,9 @@ pub enum PatternKind { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Constructor { - file_id: FileId, - item_id: TermItemId, - fields: Vec, + pub file_id: FileId, + pub item_id: TermItemId, + pub fields: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -51,107 +51,77 @@ pub enum RecordElement { } pub type PatternId = interner::Id; +pub type PatternStorage = interner::Interner; type PatternVector = Vec; type PatternMatrix = Vec; -type WitnessVector = Vec; - -#[derive(Default)] -struct ExhaustivenessState { - interner: interner::Interner, -} - -impl ExhaustivenessState { - fn allocate(&mut self, kind: PatternKind, t: TypeId) -> PatternId { - let pattern = Pattern { kind, t }; - self.interner.intern(pattern) - } +pub type WitnessVector = Vec; - fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { - self.allocate(PatternKind::Wildcard, t) - } -} - -fn lower_binder( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - context: &CheckContext, - id: BinderId, -) -> PatternId +fn lower_binder(state: &mut CheckState, context: &CheckContext, id: BinderId) -> PatternId where Q: ExternalQueries, { - let t = check_state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); + let t = state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); let Some(kind) = context.lowered.info.get_binder_kind(id) else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; match kind { lowering::BinderKind::Typed { binder, .. } => match binder { - Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), - None => exhaustiveness_state.allocate_wildcard(t), + Some(id) => lower_binder(state, context, *id), + None => state.allocate_wildcard(t), }, lowering::BinderKind::OperatorChain { .. } => { - lower_operator_chain_binder(check_state, exhaustiveness_state, context, id, t) + lower_operator_chain_binder(state, context, id, t) } lowering::BinderKind::Integer { value } => match value { - Some(v) => exhaustiveness_state.allocate(PatternKind::Integer(*v), t), - None => exhaustiveness_state.allocate_wildcard(t), + Some(v) => state.allocate_pattern(PatternKind::Integer(*v), t), + None => state.allocate_wildcard(t), }, lowering::BinderKind::Number { negative, value } => { if let Some(value) = value { let kind = PatternKind::Number(*negative, SmolStr::clone(value)); - exhaustiveness_state.allocate(kind, t) + state.allocate_pattern(kind, t) } else { - exhaustiveness_state.allocate_wildcard(t) + state.allocate_wildcard(t) } } - lowering::BinderKind::Constructor { resolution, arguments } => lower_constructor_binder( - check_state, - exhaustiveness_state, - context, - resolution, - arguments, - t, - ), - lowering::BinderKind::Variable { .. } => exhaustiveness_state.allocate_wildcard(t), + lowering::BinderKind::Constructor { resolution, arguments } => { + lower_constructor_binder(state, context, resolution, arguments, t) + } + lowering::BinderKind::Variable { .. } => state.allocate_wildcard(t), lowering::BinderKind::Named { binder, .. } => match binder { - Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), - None => exhaustiveness_state.allocate_wildcard(t), + Some(id) => lower_binder(state, context, *id), + None => state.allocate_wildcard(t), }, - lowering::BinderKind::Wildcard => exhaustiveness_state.allocate_wildcard(t), + lowering::BinderKind::Wildcard => state.allocate_wildcard(t), lowering::BinderKind::String { value, .. } => { if let Some(value) = value { let kind = PatternKind::String(SmolStr::clone(value)); - exhaustiveness_state.allocate(kind, t) + state.allocate_pattern(kind, t) } else { - exhaustiveness_state.allocate_wildcard(t) + state.allocate_wildcard(t) } } lowering::BinderKind::Char { value } => match value { - Some(v) => exhaustiveness_state.allocate(PatternKind::Char(*v), t), - None => exhaustiveness_state.allocate_wildcard(t), + Some(v) => state.allocate_pattern(PatternKind::Char(*v), t), + None => state.allocate_wildcard(t), }, lowering::BinderKind::Boolean { boolean } => { - exhaustiveness_state.allocate(PatternKind::Boolean(*boolean), t) - } - lowering::BinderKind::Array { array } => { - lower_array_binder(check_state, exhaustiveness_state, context, array, t) - } - lowering::BinderKind::Record { record } => { - lower_record_binder(check_state, exhaustiveness_state, context, record, t) + state.allocate_pattern(PatternKind::Boolean(*boolean), t) } + lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), + lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { - Some(id) => lower_binder(check_state, exhaustiveness_state, context, *id), - None => exhaustiveness_state.allocate_wildcard(t), + Some(id) => lower_binder(state, context, *id), + None => state.allocate_wildcard(t), }, } } fn lower_array_binder( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, array: &[BinderId], t: TypeId, @@ -159,16 +129,12 @@ fn lower_array_binder( where Q: ExternalQueries, { - let elements = array - .iter() - .map(|element| lower_binder(check_state, exhaustiveness_state, context, *element)) - .collect(); - exhaustiveness_state.allocate(PatternKind::Array { elements }, t) + let elements = array.iter().map(|element| lower_binder(state, context, *element)).collect(); + state.allocate_pattern(PatternKind::Array { elements }, t) } fn lower_record_binder( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, record: &[lowering::BinderRecordItem], t: TypeId, @@ -176,16 +142,13 @@ fn lower_record_binder( where Q: ExternalQueries, { - let elements = record - .iter() - .map(|element| lower_record_element(check_state, exhaustiveness_state, context, element)) - .collect(); - exhaustiveness_state.allocate(PatternKind::Record { elements }, t) + let elements = + record.iter().map(|element| lower_record_element(state, context, element)).collect(); + state.allocate_pattern(PatternKind::Record { elements }, t) } fn lower_record_element( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, element: &lowering::BinderRecordItem, ) -> RecordElement @@ -196,9 +159,9 @@ where lowering::BinderRecordItem::RecordField { name, value } => { let name = name.clone().unwrap_or(MISSING_NAME); let value = if let Some(value) = value { - lower_binder(check_state, exhaustiveness_state, context, *value) + lower_binder(state, context, *value) } else { - exhaustiveness_state.allocate_wildcard(context.prim.unknown) + state.allocate_wildcard(context.prim.unknown) }; RecordElement::Named(name, value) } @@ -210,8 +173,7 @@ where } fn lower_constructor_binder( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, resolution: &Option<(FileId, TermItemId)>, arguments: &Arc<[BinderId]>, @@ -221,21 +183,18 @@ where Q: ExternalQueries, { let Some((file_id, item_id)) = resolution else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; - let fields = arguments - .iter() - .map(|argument| lower_binder(check_state, exhaustiveness_state, context, *argument)) - .collect_vec(); + let fields = + arguments.iter().map(|argument| lower_binder(state, context, *argument)).collect_vec(); let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; - exhaustiveness_state.allocate(PatternKind::Constructor { constructor }, t) + state.allocate_pattern(PatternKind::Constructor { constructor }, t) } fn lower_operator_chain_binder( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, id: BinderId, t: TypeId, @@ -244,19 +203,18 @@ where Q: ExternalQueries, { let Some(tree) = context.bracketed.binders.get(&id) else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; let Ok(tree) = tree else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; - lower_operator_tree(check_state, exhaustiveness_state, context, tree, t) + lower_operator_tree(state, context, tree, t) } fn lower_operator_tree( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, tree: &OperatorTree, t: TypeId, @@ -265,24 +223,16 @@ where Q: ExternalQueries, { match tree { - OperatorTree::Leaf(None) => exhaustiveness_state.allocate_wildcard(t), - OperatorTree::Leaf(Some(binder_id)) => { - lower_binder(check_state, exhaustiveness_state, context, *binder_id) + OperatorTree::Leaf(None) => state.allocate_wildcard(t), + OperatorTree::Leaf(Some(binder_id)) => lower_binder(state, context, *binder_id), + OperatorTree::Branch(operator_id, children) => { + lower_operator_branch(state, context, *operator_id, children, t) } - OperatorTree::Branch(operator_id, children) => lower_operator_branch( - check_state, - exhaustiveness_state, - context, - *operator_id, - children, - t, - ), } } fn lower_operator_branch( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, operator_id: TermOperatorId, children: &[OperatorTree; 2], @@ -292,25 +242,23 @@ where Q: ExternalQueries, { let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; let Some(OperatorBranchTypes { left, right, result }) = - check_state.term_scope.lookup_operator_node(operator_id) + state.term_scope.lookup_operator_node(operator_id) else { - return exhaustiveness_state.allocate_wildcard(t); + return state.allocate_wildcard(t); }; let [left_tree, right_tree] = children; - let left_pattern = - lower_operator_tree(check_state, exhaustiveness_state, context, left_tree, left); + let left_pattern = lower_operator_tree(state, context, left_tree, left); - let right_pattern = - lower_operator_tree(check_state, exhaustiveness_state, context, right_tree, right); + let right_pattern = lower_operator_tree(state, context, right_tree, right); let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; - exhaustiveness_state.allocate(PatternKind::Constructor { constructor }, result) + state.allocate_pattern(PatternKind::Constructor { constructor }, result) } /// Determines if a [`PatternVector`] is useful with respect to a [`PatternMatrix`]. @@ -319,8 +267,7 @@ where /// any pattern vector in the matrix. This is the core algorithm from Maranget's /// "Warnings for pattern matching" paper. fn algorithm_u( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -338,32 +285,21 @@ where return Ok(false); }; - let first_pattern = exhaustiveness_state.interner[first_pattern].clone(); + let first_pattern = state.patterns[first_pattern].clone(); match first_pattern.kind { - PatternKind::Constructor { constructor } => algorithm_u_constructor( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - constructor, - ), - PatternKind::Wildcard => algorithm_u_wildcard( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - first_pattern.t, - ), - _ => algorithm_u_other(check_state, exhaustiveness_state, context, matrix, vector), + PatternKind::Constructor { constructor } => { + algorithm_u_constructor(state, context, matrix, vector, constructor) + } + PatternKind::Wildcard => { + algorithm_u_wildcard(state, context, matrix, vector, first_pattern.t) + } + _ => algorithm_u_other(state, context, matrix, vector), } } fn algorithm_u_constructor( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -372,27 +308,17 @@ fn algorithm_u_constructor( where Q: ExternalQueries, { - let specialized_matrix = - specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); - let Some(specialized_vector) = - specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) - else { + let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - algorithm_u( - check_state, - exhaustiveness_state, - context, - &specialized_matrix, - &specialized_vector, - ) + algorithm_u(state, context, &specialized_matrix, &specialized_vector) } fn algorithm_u_wildcard( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -401,40 +327,31 @@ fn algorithm_u_wildcard( where Q: ExternalQueries, { - let sigma = collect_sigma(check_state, exhaustiveness_state, context, matrix, first_type)?; + let sigma = collect_sigma(state, context, matrix, first_type)?; let complete = sigma_is_complete(context, &sigma)?; if complete { // If sigma is complete, check if useful for any constructor for constructor in sigma.constructors { - let specialized_matrix = - specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); - let specialized_vector = - specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) - .expect("specialising wildcard head must succeed"); - - if algorithm_u( - check_state, - exhaustiveness_state, - context, - &specialized_matrix, - &specialized_vector, - )? { + let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + let specialized_vector = specialise_vector(state, context, &constructor, vector) + .expect("specialising wildcard head must succeed"); + + if algorithm_u(state, context, &specialized_matrix, &specialized_vector)? { return Ok(true); } } Ok(false) } else { // If sigma is incomplete, use default matrix - let default = default_matrix(exhaustiveness_state, matrix); + let default = default_matrix(state, matrix); let tail = vector[1..].to_vec(); - algorithm_u(check_state, exhaustiveness_state, context, &default, &tail) + algorithm_u(state, context, &default, &tail) } } fn algorithm_u_other( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -443,9 +360,9 @@ where Q: ExternalQueries, { // For literals and other patterns, treat as incomplete sigma (conservative) - let default = default_matrix(exhaustiveness_state, matrix); + let default = default_matrix(state, matrix); let tail = vector[1..].to_vec(); - algorithm_u(check_state, exhaustiveness_state, context, &default, &tail) + algorithm_u(state, context, &default, &tail) } /// Determines the matching [`WitnessVector`] given a [`PatternMatrix`] @@ -466,8 +383,7 @@ where /// these witnesses as it compares the constructors that appear in the /// matrix against the constructors available in the checking environment. fn algorithm_m( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -486,34 +402,16 @@ where return Ok(None); }; - let first_pattern = exhaustiveness_state.interner[first_pattern].clone(); + let first_pattern = state.patterns[first_pattern].clone(); match first_pattern.kind { - PatternKind::Constructor { constructor } => algorithm_m_constructor( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - constructor, - first_pattern.t, - ), - PatternKind::Wildcard => algorithm_m_wildcard( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - first_pattern.t, - ), - _ => algorithm_m_other( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - first_pattern.t, - ), + PatternKind::Constructor { constructor } => { + algorithm_m_constructor(state, context, matrix, vector, constructor, first_pattern.t) + } + PatternKind::Wildcard => { + algorithm_m_wildcard(state, context, matrix, vector, first_pattern.t) + } + _ => algorithm_m_other(state, context, matrix, vector, first_pattern.t), } } @@ -529,8 +427,7 @@ where /// See documentation for [`specialise_matrix`] and [`specialise_vector`] for /// more information on what specialisation entails given a constructor. fn algorithm_m_constructor( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -542,22 +439,13 @@ where { let arity = constructor.fields.len(); - let specialized_matrix = - specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); - let Some(specialized_vector) = - specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) - else { + let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - let witnesses = algorithm_m( - check_state, - exhaustiveness_state, - context, - &specialized_matrix, - &specialized_vector, - )?; + let witnesses = algorithm_m(state, context, &specialized_matrix, &specialized_vector)?; let Some(witnesses) = witnesses else { return Ok(None); @@ -574,7 +462,7 @@ where }, }; - let constructor = exhaustiveness_state.allocate(pattern, first_type); + let constructor = state.allocate_pattern(pattern, first_type); let tail_columns = tail_columns.iter().copied(); iter::once(constructor).chain(tail_columns).collect() @@ -609,8 +497,7 @@ where /// reporting pattern warnings. Otherwise, if the sigma is empty, we /// simply produce a wildcard pattern. fn algorithm_m_wildcard( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -619,37 +506,20 @@ fn algorithm_m_wildcard( where Q: ExternalQueries, { - let sigma = collect_sigma(check_state, exhaustiveness_state, context, matrix, first_type)?; + let sigma = collect_sigma(state, context, matrix, first_type)?; let complete = sigma_is_complete(context, &sigma)?; let Sigma { constructors, missing } = sigma; if complete { - algorithm_m_wildcard_complete( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - first_type, - constructors, - ) + algorithm_m_wildcard_complete(state, context, matrix, vector, first_type, constructors) } else { - algorithm_m_wildcard_incomplete( - check_state, - exhaustiveness_state, - context, - matrix, - vector, - first_type, - &missing, - ) + algorithm_m_wildcard_incomplete(state, context, matrix, vector, first_type, &missing) } } fn algorithm_m_wildcard_complete( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -664,22 +534,16 @@ where for constructor in sigma { let arity = constructor.fields.len(); - let specialized_matrix = - specialise_matrix(check_state, exhaustiveness_state, context, &constructor, matrix); + let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); - let Some(specialized_vector) = - specialise_vector(check_state, exhaustiveness_state, context, &constructor, vector) + let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - if let Some(witnesses) = algorithm_m( - check_state, - exhaustiveness_state, - context, - &specialized_matrix, - &specialized_vector, - )? { + if let Some(witnesses) = + algorithm_m(state, context, &specialized_matrix, &specialized_vector)? + { for witness in witnesses { let (argument_columns, tail_columns) = witness.split_at(arity); @@ -691,7 +555,7 @@ where }, }; - let constructor = exhaustiveness_state.allocate(pattern, first_type); + let constructor = state.allocate_pattern(pattern, first_type); let tail_columns = tail_columns.iter().copied(); let witnesses = iter::once(constructor).chain(tail_columns).collect(); @@ -704,8 +568,7 @@ where } fn algorithm_m_wildcard_incomplete( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -715,11 +578,10 @@ fn algorithm_m_wildcard_incomplete( where Q: ExternalQueries, { - let default = default_matrix(exhaustiveness_state, matrix); + let default = default_matrix(state, matrix); let tail_columns = vector[1..].to_vec(); - let witnesses = - algorithm_m(check_state, exhaustiveness_state, context, &default, &tail_columns)?; + let witnesses = algorithm_m(state, context, &default, &tail_columns)?; let Some(witnesses) = witnesses else { return Ok(None); @@ -727,11 +589,8 @@ where let head = if let Some(missing_constructor) = missing.first() { // Use constructor field types when available; fall back to `prim.unknown`. - let fields = missing_constructor - .fields - .iter() - .map(|&t| exhaustiveness_state.allocate_wildcard(t)) - .collect_vec(); + let fields = + missing_constructor.fields.iter().map(|&t| state.allocate_wildcard(t)).collect_vec(); let constructor = Constructor { file_id: missing_constructor.file_id, @@ -740,9 +599,9 @@ where }; let pattern = PatternKind::Constructor { constructor }; - exhaustiveness_state.allocate(pattern, first_type) + state.allocate_pattern(pattern, first_type) } else { - exhaustiveness_state.allocate_wildcard(first_type) + state.allocate_wildcard(first_type) }; Ok(Some( @@ -751,8 +610,7 @@ where } fn algorithm_m_other( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, @@ -762,24 +620,23 @@ where Q: ExternalQueries, { // For literals and other patterns, treat as incomplete sigma (conservative) - let default = default_matrix(exhaustiveness_state, matrix); + let default = default_matrix(state, matrix); let tail = vector[1..].to_vec(); - let witnesses = algorithm_m(check_state, exhaustiveness_state, context, &default, &tail)?; + let witnesses = algorithm_m(state, context, &default, &tail)?; let Some(witnesses) = witnesses else { return Ok(None); }; // Prefix with wildcard - let head = exhaustiveness_state.allocate_wildcard(first_type); + let head = state.allocate_wildcard(first_type); Ok(Some(witnesses.into_iter().map(|w| iter::once(head).chain(w).collect()).collect())) } /// Specialises a [`PatternMatrix`] given a [`Constructor`]. fn specialise_matrix( - check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, expected: &Constructor, matrix: &PatternMatrix, @@ -787,33 +644,32 @@ fn specialise_matrix( where Q: ExternalQueries, { - let matrix = matrix.iter().filter_map(|row| { - specialise_vector(check_state, exhaustiveness_state, context, expected, row) - }); + let matrix = matrix.iter().filter_map(|row| specialise_vector(state, context, expected, row)); matrix.collect() } /// Specialises a [`PatternVector`] given a [`Constructor`]. fn specialise_vector( - _check_state: &mut CheckState, - exhaustiveness_state: &mut ExhaustivenessState, - _context: &CheckContext, + state: &mut CheckState, + context: &CheckContext, expected: &Constructor, vector: &PatternVector, ) -> Option where Q: ExternalQueries, { + let _ = context; + let [first_column, ref tail_columns @ ..] = vector[..] else { unreachable!("invariant violated: specialise_vector processed empty row"); }; - let first_column = &exhaustiveness_state.interner[first_column]; + let first_column = &state.patterns[first_column]; if let PatternKind::Wildcard = first_column.kind { let wildcards = expected.fields.iter().map(|&pattern| { - let t = exhaustiveness_state.interner[pattern].t; - exhaustiveness_state.allocate_wildcard(t) + let t = state.patterns[pattern].t; + state.allocate_wildcard(t) }); let tail_columns = tail_columns.iter().copied(); return Some(iter::chain(wildcards, tail_columns).collect()); @@ -830,15 +686,12 @@ where Some(iter::chain(&constructor.fields, tail_columns).copied().collect()) } -fn default_matrix( - exhaustiveness_state: &ExhaustivenessState, - matrix: &PatternMatrix, -) -> PatternMatrix { +fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { let filter_map = matrix.iter().filter_map(|row| { let [first_column, ref default_columns @ ..] = row[..] else { unreachable!("invariant violated: default_matrix processed empty row"); }; - if let PatternKind::Wildcard = exhaustiveness_state.interner[first_column].kind { + if let PatternKind::Wildcard = state.patterns[first_column].kind { Some(default_columns.to_vec()) } else { None @@ -870,8 +723,7 @@ struct MissingConstructor { /// representative `Constructor` per distinct constructor id. Non-constructor /// patterns (wildcards, literals, etc.) are ignored. fn collect_sigma( - check_state: &mut CheckState, - exhaustiveness_state: &ExhaustivenessState, + state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, scrutinee_type: TypeId, @@ -886,7 +738,7 @@ where let [first_column, ..] = row[..] else { continue; }; - let pattern = &exhaustiveness_state.interner[first_column]; + let pattern = &state.patterns[first_column]; if let PatternKind::Constructor { constructor } = &pattern.kind { let key = ConstructorKey(constructor.file_id, constructor.item_id); if seen.insert(key) { @@ -895,8 +747,7 @@ where } } - let missing = - collect_missing_constructors(check_state, context, scrutinee_type, &constructors)?; + let missing = collect_missing_constructors(state, context, scrutinee_type, &constructors)?; Ok(Sigma { constructors, missing }) } @@ -932,7 +783,7 @@ where } fn collect_missing_constructors( - check_state: &mut CheckState, + state: &mut CheckState, context: &CheckContext, scrutinee_type: TypeId, constructors: &[Constructor], @@ -951,14 +802,13 @@ where }; let sigma: FxHashSet = constructors.iter().map(|c| c.item_id).collect(); - let arguments = toolkit::extract_all_applications(check_state, scrutinee_type); + let arguments = toolkit::extract_all_applications(state, scrutinee_type); let mut missing = vec![]; for item_id in indexed.pairs.data_constructors(type_item_id) { let file_id = constructor.file_id; if !sigma.contains(&item_id) { - let fields = - constructor_field_types(check_state, context, file_id, item_id, &arguments)?; + let fields = constructor_field_types(state, context, file_id, item_id, &arguments)?; missing.push(MissingConstructor { file_id, item_id, fields }); } } @@ -967,7 +817,7 @@ where } fn constructor_field_types( - check_state: &mut CheckState, + state: &mut CheckState, context: &CheckContext, file_id: FileId, term_id: TermItemId, @@ -976,15 +826,14 @@ fn constructor_field_types( where Q: ExternalQueries, { - let constructor_type = derive::lookup_local_term_type(check_state, context, file_id, term_id)?; + let constructor_type = derive::lookup_local_term_type(state, context, file_id, term_id)?; if let Some(constructor_type) = constructor_type { - let constructor = - toolkit::instantiate_with_arguments(check_state, constructor_type, arguments); - let (fields, _) = toolkit::extract_function_arguments(check_state, constructor); + let constructor = toolkit::instantiate_with_arguments(state, constructor_type, arguments); + let (fields, _) = toolkit::extract_function_arguments(state, constructor); Ok(fields) } else { let arity = get_constructor_arity(context, file_id, term_id)?; - Ok(iter::repeat(context.prim.unknown).take(arity).collect()) + Ok(iter::repeat_n(context.prim.unknown, arity).collect()) } } @@ -1026,7 +875,7 @@ pub struct CasePatternReport { /// Only unconditional branches are counted toward these checks, as pattern /// guards do not guarantee coverage. pub fn check_case_patterns( - check_state: &mut CheckState, + state: &mut CheckState, context: &CheckContext, trunk_types: &[TypeId], branches: &[lowering::CaseBranch], @@ -1038,8 +887,6 @@ where return Ok(CasePatternReport { missing: None, redundant: vec![] }); } - let mut exhaustiveness_state = ExhaustivenessState::default(); - // Build pattern matrix from unconditional branches only. let mut rows: Vec = vec![]; for branch in branches { @@ -1053,12 +900,12 @@ where let mut row: PatternVector = vec![]; for &binder_id in branch.binders.iter() { - let pattern = lower_binder(check_state, &mut exhaustiveness_state, context, binder_id); + let pattern = lower_binder(state, context, binder_id); row.push(pattern); } while row.len() < trunk_types.len() { - let wildcard = exhaustiveness_state.allocate_wildcard(trunk_types[row.len()]); + let wildcard = state.allocate_wildcard(trunk_types[row.len()]); row.push(wildcard); } @@ -1070,23 +917,22 @@ where let mut redundant = vec![]; let mut matrix: PatternMatrix = vec![]; for row in &rows { - let useful = algorithm_u(check_state, &mut exhaustiveness_state, context, &matrix, row)?; + let useful = algorithm_u(state, context, &matrix, row)?; if useful { matrix.push(row.clone()); } else { - redundant.push(render::render_witness(context, &exhaustiveness_state, row)); + redundant.push(pretty::pretty_witness(context, state, row)); } } - let query: PatternVector = - trunk_types.iter().map(|&t| exhaustiveness_state.allocate_wildcard(t)).collect(); + let query: PatternVector = trunk_types.iter().map(|&t| state.allocate_wildcard(t)).collect(); - let witnesses = algorithm_m(check_state, &mut exhaustiveness_state, context, &rows, &query)?; + let witnesses = algorithm_m(state, context, &rows, &query)?; let missing = witnesses.map(|witnesses| { witnesses .iter() .take(5) - .map(|witness| render::render_witness(context, &exhaustiveness_state, witness)) + .map(|witness| pretty::pretty_witness(context, state, witness)) .collect() }); diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/render.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs similarity index 74% rename from compiler-core/checking/src/algorithm/exhaustiveness/render.rs rename to compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index d0d79baa..20c5dd4a 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/render.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -6,30 +6,26 @@ use itertools::Itertools; use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{ - Constructor, ExhaustivenessState, PatternId, PatternKind, RecordElement, WitnessVector, + Constructor, PatternId, PatternKind, RecordElement, WitnessVector, }; -use crate::algorithm::state::CheckContext; +use crate::algorithm::state::{CheckContext, CheckState}; -pub fn render_witness( +pub fn pretty_witness( context: &CheckContext, - state: &ExhaustivenessState, + state: &CheckState, witness: &WitnessVector, ) -> String where Q: ExternalQueries, { - witness.iter().map(|&id| render_pattern(context, state, id)).join(", ") + witness.iter().map(|&id| pretty_pattern(context, state, id)).join(", ") } -fn render_pattern( - context: &CheckContext, - state: &ExhaustivenessState, - id: PatternId, -) -> String +fn pretty_pattern(context: &CheckContext, state: &CheckState, id: PatternId) -> String where Q: ExternalQueries, { - let pattern = &state.interner[id]; + let pattern = &state.patterns[id]; match &pattern.kind { PatternKind::Wildcard => "_".to_string(), PatternKind::Boolean(b) => b.to_string(), @@ -44,26 +40,26 @@ where } } PatternKind::Array { elements } => { - let mut elements = elements.iter().map(|&e| render_pattern(context, state, e)); + let mut elements = elements.iter().map(|&e| pretty_pattern(context, state, e)); format!("[{}]", elements.join(", ")) } PatternKind::Record { elements } => { let mut elements = elements.iter().map(|e| match e { RecordElement::Named(name, pattern) => { - let pattern = render_pattern(context, state, *pattern); + let pattern = pretty_pattern(context, state, *pattern); format!("{name}: {pattern}") } RecordElement::Pun(name) => name.to_string(), }); format!("{{ {} }}", elements.join(", ")) } - PatternKind::Constructor { constructor } => render_constructor(context, state, constructor), + PatternKind::Constructor { constructor } => pretty_constructor(context, state, constructor), } } -fn render_constructor( +fn pretty_constructor( context: &CheckContext, - exhaustiveness_state: &ExhaustivenessState, + state: &CheckState, constructor: &Constructor, ) -> String where @@ -77,8 +73,8 @@ where } let mut fields = constructor.fields.iter().map(|&id| { - let rendered = render_pattern(context, exhaustiveness_state, id); - let pattern = &exhaustiveness_state.interner[id]; + let rendered = pretty_pattern(context, state, id); + let pattern = &state.patterns[id]; if let PatternKind::Constructor { constructor } = &pattern.kind && !constructor.fields.is_empty() { diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 943ea9bb..a06bd436 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -19,6 +19,7 @@ use smol_str::ToSmolStr; use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; +use crate::algorithm::exhaustiveness::{Pattern, PatternId, PatternKind, PatternStorage}; use crate::algorithm::{constraint, transfer}; use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; use crate::error::{CheckError, ErrorKind, ErrorStep}; @@ -316,6 +317,9 @@ pub struct CheckState { /// Flag that determines when it's appropriate to expand synonyms. pub defer_synonym_expansion: bool, + + /// Interns patterns for exhaustiveness checking. + pub patterns: PatternStorage, } #[derive(Clone)] @@ -1117,3 +1121,14 @@ impl CheckState { }) } } + +impl CheckState { + pub fn allocate_pattern(&mut self, kind: PatternKind, t: TypeId) -> PatternId { + let pattern = Pattern { kind, t }; + self.patterns.intern(pattern) + } + + pub fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { + self.allocate_pattern(PatternKind::Wildcard, t) + } +} From f57b9023ce66a559669e264f5406d275db2237bc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 31 Jan 2026 22:53:43 +0800 Subject: [PATCH 079/386] Adjustments to exhaustiveness implementation --- .../checking/src/algorithm/exhaustiveness.rs | 99 ++++++++++++------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 282e9ed1..147505db 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -331,23 +331,49 @@ where let complete = sigma_is_complete(context, &sigma)?; if complete { - // If sigma is complete, check if useful for any constructor - for constructor in sigma.constructors { - let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); - let specialized_vector = specialise_vector(state, context, &constructor, vector) - .expect("specialising wildcard head must succeed"); - - if algorithm_u(state, context, &specialized_matrix, &specialized_vector)? { - return Ok(true); - } - } - Ok(false) + algorithm_u_complete(state, context, matrix, vector, sigma) } else { - // If sigma is incomplete, use default matrix - let default = default_matrix(state, matrix); - let tail = vector[1..].to_vec(); - algorithm_u(state, context, &default, &tail) + algorithm_u_wildcard_incomplete(state, context, matrix, vector) + } +} + +fn algorithm_u_complete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + sigma: Sigma, +) -> QueryResult +where + Q: ExternalQueries, +{ + for constructor in sigma.constructors { + let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + + let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) + else { + unreachable!("invariant violated: vector contains constructor"); + }; + + if algorithm_u(state, context, &specialized_matrix, &specialized_vector)? { + return Ok(true); + } } + Ok(false) +} + +fn algorithm_u_wildcard_incomplete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult +where + Q: ExternalQueries, +{ + let default = default_matrix(state, matrix); + let tail_columns = vector[1..].to_vec(); + algorithm_u(state, context, &default, &tail_columns) } fn algorithm_u_other( @@ -361,8 +387,8 @@ where { // For literals and other patterns, treat as incomplete sigma (conservative) let default = default_matrix(state, matrix); - let tail = vector[1..].to_vec(); - algorithm_u(state, context, &default, &tail) + let tail_columns = vector[1..].to_vec(); + algorithm_u(state, context, &default, &tail_columns) } /// Determines the matching [`WitnessVector`] given a [`PatternMatrix`] @@ -508,13 +534,10 @@ where { let sigma = collect_sigma(state, context, matrix, first_type)?; let complete = sigma_is_complete(context, &sigma)?; - - let Sigma { constructors, missing } = sigma; - if complete { - algorithm_m_wildcard_complete(state, context, matrix, vector, first_type, constructors) + algorithm_m_wildcard_complete(state, context, matrix, vector, first_type, &sigma) } else { - algorithm_m_wildcard_incomplete(state, context, matrix, vector, first_type, &missing) + algorithm_m_wildcard_incomplete(state, context, matrix, vector, first_type, &sigma) } } @@ -524,14 +547,14 @@ fn algorithm_m_wildcard_complete( matrix: &PatternMatrix, vector: &PatternVector, first_type: TypeId, - sigma: Vec, + sigma: &Sigma, ) -> QueryResult>> where Q: ExternalQueries, { let mut all_witnesses = vec![]; - for constructor in sigma { + for constructor in &sigma.constructors { let arity = constructor.fields.len(); let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); @@ -573,7 +596,7 @@ fn algorithm_m_wildcard_incomplete( matrix: &PatternMatrix, vector: &PatternVector, first_type: TypeId, - missing: &[MissingConstructor], + sigma: &Sigma, ) -> QueryResult>> where Q: ExternalQueries, @@ -587,26 +610,28 @@ where return Ok(None); }; - let head = if let Some(missing_constructor) = missing.first() { - // Use constructor field types when available; fall back to `prim.unknown`. - let fields = - missing_constructor.fields.iter().map(|&t| state.allocate_wildcard(t)).collect_vec(); + let first_column = if let Some(constructor) = sigma.missing.first() { + let fields = constructor.fields.iter().map(|&t| state.allocate_wildcard(t)).collect_vec(); - let constructor = Constructor { - file_id: missing_constructor.file_id, - item_id: missing_constructor.item_id, - fields, + let pattern = PatternKind::Constructor { + constructor: Constructor { + file_id: constructor.file_id, + item_id: constructor.item_id, + fields, + }, }; - let pattern = PatternKind::Constructor { constructor }; state.allocate_pattern(pattern, first_type) } else { state.allocate_wildcard(first_type) }; - Ok(Some( - witnesses.into_iter().map(|witness| iter::once(head).chain(witness).collect()).collect(), - )) + let witness = witnesses + .into_iter() + .map(|witness| iter::once(first_column).chain(witness).collect()) + .collect(); + + Ok(Some(witness)) } fn algorithm_m_other( From 554b07c3c36beaf0819d4985a6fd65b09f22f422 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Feb 2026 00:01:13 +0800 Subject: [PATCH 080/386] Move conversion into submodule --- .../checking/src/algorithm/exhaustiveness.rs | 240 ++---------------- .../src/algorithm/exhaustiveness/convert.rs | 221 ++++++++++++++++ 2 files changed, 236 insertions(+), 225 deletions(-) create mode 100644 compiler-core/checking/src/algorithm/exhaustiveness/convert.rs diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 147505db..4f26d1ad 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -1,23 +1,19 @@ +mod convert; mod pretty; use std::iter; -use std::sync::Arc; use building_types::QueryResult; use files::FileId; use indexing::TermItemId; use itertools::Itertools; -use lowering::{BinderId, TermOperatorId}; use rustc_hash::FxHashSet; use smol_str::SmolStr; -use sugar::OperatorTree; -use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; +use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{derive, toolkit}; use crate::{ExternalQueries, TypeId}; -const MISSING_NAME: SmolStr = SmolStr::new_inline(""); - #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Pattern { pub kind: PatternKind, @@ -57,210 +53,6 @@ type PatternVector = Vec; type PatternMatrix = Vec; pub type WitnessVector = Vec; -fn lower_binder(state: &mut CheckState, context: &CheckContext, id: BinderId) -> PatternId -where - Q: ExternalQueries, -{ - let t = state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); - - let Some(kind) = context.lowered.info.get_binder_kind(id) else { - return state.allocate_wildcard(t); - }; - - match kind { - lowering::BinderKind::Typed { binder, .. } => match binder { - Some(id) => lower_binder(state, context, *id), - None => state.allocate_wildcard(t), - }, - lowering::BinderKind::OperatorChain { .. } => { - lower_operator_chain_binder(state, context, id, t) - } - lowering::BinderKind::Integer { value } => match value { - Some(v) => state.allocate_pattern(PatternKind::Integer(*v), t), - None => state.allocate_wildcard(t), - }, - lowering::BinderKind::Number { negative, value } => { - if let Some(value) = value { - let kind = PatternKind::Number(*negative, SmolStr::clone(value)); - state.allocate_pattern(kind, t) - } else { - state.allocate_wildcard(t) - } - } - lowering::BinderKind::Constructor { resolution, arguments } => { - lower_constructor_binder(state, context, resolution, arguments, t) - } - lowering::BinderKind::Variable { .. } => state.allocate_wildcard(t), - lowering::BinderKind::Named { binder, .. } => match binder { - Some(id) => lower_binder(state, context, *id), - None => state.allocate_wildcard(t), - }, - lowering::BinderKind::Wildcard => state.allocate_wildcard(t), - lowering::BinderKind::String { value, .. } => { - if let Some(value) = value { - let kind = PatternKind::String(SmolStr::clone(value)); - state.allocate_pattern(kind, t) - } else { - state.allocate_wildcard(t) - } - } - lowering::BinderKind::Char { value } => match value { - Some(v) => state.allocate_pattern(PatternKind::Char(*v), t), - None => state.allocate_wildcard(t), - }, - lowering::BinderKind::Boolean { boolean } => { - state.allocate_pattern(PatternKind::Boolean(*boolean), t) - } - lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), - lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), - lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { - Some(id) => lower_binder(state, context, *id), - None => state.allocate_wildcard(t), - }, - } -} - -fn lower_array_binder( - state: &mut CheckState, - context: &CheckContext, - array: &[BinderId], - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - let elements = array.iter().map(|element| lower_binder(state, context, *element)).collect(); - state.allocate_pattern(PatternKind::Array { elements }, t) -} - -fn lower_record_binder( - state: &mut CheckState, - context: &CheckContext, - record: &[lowering::BinderRecordItem], - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - let elements = - record.iter().map(|element| lower_record_element(state, context, element)).collect(); - state.allocate_pattern(PatternKind::Record { elements }, t) -} - -fn lower_record_element( - state: &mut CheckState, - context: &CheckContext, - element: &lowering::BinderRecordItem, -) -> RecordElement -where - Q: ExternalQueries, -{ - match element { - lowering::BinderRecordItem::RecordField { name, value } => { - let name = name.clone().unwrap_or(MISSING_NAME); - let value = if let Some(value) = value { - lower_binder(state, context, *value) - } else { - state.allocate_wildcard(context.prim.unknown) - }; - RecordElement::Named(name, value) - } - lowering::BinderRecordItem::RecordPun { name, .. } => { - let name = name.clone().unwrap_or(MISSING_NAME); - RecordElement::Pun(name) - } - } -} - -fn lower_constructor_binder( - state: &mut CheckState, - context: &CheckContext, - resolution: &Option<(FileId, TermItemId)>, - arguments: &Arc<[BinderId]>, - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - let Some((file_id, item_id)) = resolution else { - return state.allocate_wildcard(t); - }; - - let fields = - arguments.iter().map(|argument| lower_binder(state, context, *argument)).collect_vec(); - - let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; - state.allocate_pattern(PatternKind::Constructor { constructor }, t) -} - -fn lower_operator_chain_binder( - state: &mut CheckState, - context: &CheckContext, - id: BinderId, - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - let Some(tree) = context.bracketed.binders.get(&id) else { - return state.allocate_wildcard(t); - }; - - let Ok(tree) = tree else { - return state.allocate_wildcard(t); - }; - - lower_operator_tree(state, context, tree, t) -} - -fn lower_operator_tree( - state: &mut CheckState, - context: &CheckContext, - tree: &OperatorTree, - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - match tree { - OperatorTree::Leaf(None) => state.allocate_wildcard(t), - OperatorTree::Leaf(Some(binder_id)) => lower_binder(state, context, *binder_id), - OperatorTree::Branch(operator_id, children) => { - lower_operator_branch(state, context, *operator_id, children, t) - } - } -} - -fn lower_operator_branch( - state: &mut CheckState, - context: &CheckContext, - operator_id: TermOperatorId, - children: &[OperatorTree; 2], - t: TypeId, -) -> PatternId -where - Q: ExternalQueries, -{ - let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { - return state.allocate_wildcard(t); - }; - - let Some(OperatorBranchTypes { left, right, result }) = - state.term_scope.lookup_operator_node(operator_id) - else { - return state.allocate_wildcard(t); - }; - - let [left_tree, right_tree] = children; - - let left_pattern = lower_operator_tree(state, context, left_tree, left); - - let right_pattern = lower_operator_tree(state, context, right_tree, right); - - let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; - state.allocate_pattern(PatternKind::Constructor { constructor }, result) -} - /// Determines if a [`PatternVector`] is useful with respect to a [`PatternMatrix`]. /// /// A pattern vector is useful if it matches at least one value not matched by @@ -557,9 +349,9 @@ where for constructor in &sigma.constructors { let arity = constructor.fields.len(); - let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + let specialized_matrix = specialise_matrix(state, context, constructor, matrix); - let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) + let Some(specialized_vector) = specialise_vector(state, context, constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; @@ -912,8 +704,7 @@ where return Ok(CasePatternReport { missing: None, redundant: vec![] }); } - // Build pattern matrix from unconditional branches only. - let mut rows: Vec = vec![]; + let mut unconditional = vec![]; for branch in branches { let is_unconditional = matches!( &branch.guarded_expression, @@ -925,34 +716,33 @@ where let mut row: PatternVector = vec![]; for &binder_id in branch.binders.iter() { - let pattern = lower_binder(state, context, binder_id); - row.push(pattern); + row.push(convert::convert_binder(state, context, binder_id)); } while row.len() < trunk_types.len() { - let wildcard = state.allocate_wildcard(trunk_types[row.len()]); - row.push(wildcard); + let t = trunk_types[row.len()]; + row.push(state.allocate_wildcard(t)); } if !row.is_empty() { - rows.push(row); + unconditional.push(row); } } let mut redundant = vec![]; - let mut matrix: PatternMatrix = vec![]; - for row in &rows { - let useful = algorithm_u(state, context, &matrix, row)?; + let mut matrix = vec![]; + for vector in &unconditional { + let useful = algorithm_u(state, context, &matrix, vector)?; if useful { - matrix.push(row.clone()); + matrix.push(PatternVector::clone(vector)); } else { - redundant.push(pretty::pretty_witness(context, state, row)); + redundant.push(pretty::pretty_witness(context, state, vector)); } } let query: PatternVector = trunk_types.iter().map(|&t| state.allocate_wildcard(t)).collect(); - let witnesses = algorithm_m(state, context, &rows, &query)?; + let witnesses = algorithm_m(state, context, &unconditional, &query)?; let missing = witnesses.map(|witnesses| { witnesses .iter() diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs new file mode 100644 index 00000000..533dc188 --- /dev/null +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -0,0 +1,221 @@ +use std::sync::Arc; + +use files::FileId; +use indexing::TermItemId; +use itertools::Itertools; +use lowering::{BinderId, TermOperatorId}; +use smol_str::SmolStr; +use sugar::OperatorTree; + +use crate::algorithm::exhaustiveness::{Constructor, PatternId, PatternKind, RecordElement}; +use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; +use crate::{ExternalQueries, TypeId}; + +const MISSING_NAME: SmolStr = SmolStr::new_inline(""); + +pub fn convert_binder( + state: &mut CheckState, + context: &CheckContext, + id: BinderId, +) -> PatternId +where + Q: ExternalQueries, +{ + let t = state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); + + let Some(kind) = context.lowered.info.get_binder_kind(id) else { + return state.allocate_wildcard(t); + }; + + match kind { + lowering::BinderKind::Typed { binder, .. } => match binder { + Some(id) => convert_binder(state, context, *id), + None => state.allocate_wildcard(t), + }, + lowering::BinderKind::OperatorChain { .. } => { + convert_operator_chain_binder(state, context, id, t) + } + lowering::BinderKind::Integer { value } => match value { + Some(v) => state.allocate_pattern(PatternKind::Integer(*v), t), + None => state.allocate_wildcard(t), + }, + lowering::BinderKind::Number { negative, value } => { + if let Some(value) = value { + let kind = PatternKind::Number(*negative, SmolStr::clone(value)); + state.allocate_pattern(kind, t) + } else { + state.allocate_wildcard(t) + } + } + lowering::BinderKind::Constructor { resolution, arguments } => { + convert_constructor_binder(state, context, resolution, arguments, t) + } + lowering::BinderKind::Variable { .. } => state.allocate_wildcard(t), + lowering::BinderKind::Named { binder, .. } => match binder { + Some(id) => convert_binder(state, context, *id), + None => state.allocate_wildcard(t), + }, + lowering::BinderKind::Wildcard => state.allocate_wildcard(t), + lowering::BinderKind::String { value, .. } => { + if let Some(value) = value { + let kind = PatternKind::String(SmolStr::clone(value)); + state.allocate_pattern(kind, t) + } else { + state.allocate_wildcard(t) + } + } + lowering::BinderKind::Char { value } => match value { + Some(v) => state.allocate_pattern(PatternKind::Char(*v), t), + None => state.allocate_wildcard(t), + }, + lowering::BinderKind::Boolean { boolean } => { + state.allocate_pattern(PatternKind::Boolean(*boolean), t) + } + lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), + lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), + lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { + Some(id) => convert_binder(state, context, *id), + None => state.allocate_wildcard(t), + }, + } +} + +fn lower_array_binder( + state: &mut CheckState, + context: &CheckContext, + array: &[BinderId], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let elements = array.iter().map(|element| convert_binder(state, context, *element)).collect(); + state.allocate_pattern(PatternKind::Array { elements }, t) +} + +fn lower_record_binder( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::BinderRecordItem], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let elements = + record.iter().map(|element| lower_record_element(state, context, element)).collect(); + state.allocate_pattern(PatternKind::Record { elements }, t) +} + +fn lower_record_element( + state: &mut CheckState, + context: &CheckContext, + element: &lowering::BinderRecordItem, +) -> RecordElement +where + Q: ExternalQueries, +{ + match element { + lowering::BinderRecordItem::RecordField { name, value } => { + let name = name.clone().unwrap_or(MISSING_NAME); + let value = if let Some(value) = value { + convert_binder(state, context, *value) + } else { + state.allocate_wildcard(context.prim.unknown) + }; + RecordElement::Named(name, value) + } + lowering::BinderRecordItem::RecordPun { name, .. } => { + let name = name.clone().unwrap_or(MISSING_NAME); + RecordElement::Pun(name) + } + } +} + +fn convert_constructor_binder( + state: &mut CheckState, + context: &CheckContext, + resolution: &Option<(FileId, TermItemId)>, + arguments: &Arc<[BinderId]>, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let Some((file_id, item_id)) = resolution else { + return state.allocate_wildcard(t); + }; + + let fields = + arguments.iter().map(|argument| convert_binder(state, context, *argument)).collect_vec(); + + let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; + state.allocate_pattern(PatternKind::Constructor { constructor }, t) +} + +fn convert_operator_chain_binder( + state: &mut CheckState, + context: &CheckContext, + id: BinderId, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let Some(tree) = context.bracketed.binders.get(&id) else { + return state.allocate_wildcard(t); + }; + + let Ok(tree) = tree else { + return state.allocate_wildcard(t); + }; + + convert_operator_tree(state, context, tree, t) +} + +fn convert_operator_tree( + state: &mut CheckState, + context: &CheckContext, + tree: &OperatorTree, + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + match tree { + OperatorTree::Leaf(None) => state.allocate_wildcard(t), + OperatorTree::Leaf(Some(binder_id)) => convert_binder(state, context, *binder_id), + OperatorTree::Branch(operator_id, children) => { + convert_operator_branch(state, context, *operator_id, children, t) + } + } +} + +fn convert_operator_branch( + state: &mut CheckState, + context: &CheckContext, + operator_id: TermOperatorId, + children: &[OperatorTree; 2], + t: TypeId, +) -> PatternId +where + Q: ExternalQueries, +{ + let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { + return state.allocate_wildcard(t); + }; + + let Some(OperatorBranchTypes { left, right, result }) = + state.term_scope.lookup_operator_node(operator_id) + else { + return state.allocate_wildcard(t); + }; + + let [left_tree, right_tree] = children; + + let left_pattern = convert_operator_tree(state, context, left_tree, left); + let right_pattern = convert_operator_tree(state, context, right_tree, right); + + let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; + state.allocate_pattern(PatternKind::Constructor { constructor }, result) +} From e483771147d5657fe37dd8c7485475fbc380fcab Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Feb 2026 11:25:57 +0800 Subject: [PATCH 081/386] Update documentation for algorithms --- .../checking/src/algorithm/exhaustiveness.rs | 119 ++++++++++-------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 4f26d1ad..bf426902 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -56,8 +56,10 @@ pub type WitnessVector = Vec; /// Determines if a [`PatternVector`] is useful with respect to a [`PatternMatrix`]. /// /// A pattern vector is useful if it matches at least one value not matched by -/// any pattern vector in the matrix. This is the core algorithm from Maranget's -/// "Warnings for pattern matching" paper. +/// any pattern vector in the matrix. This is one of the core algorithms from +/// Maranget's "Warnings for pattern matching" paper. +/// +/// See [`algorithm_u_constructor`] and [`algorithm_u_wildcard`] for reference. fn algorithm_u( state: &mut CheckState, context: &CheckContext, @@ -90,6 +92,11 @@ where } } +/// Induction 1 +/// +/// This function uses specialisation to spread the provided [`Constructor`] +/// over both the [`PatternMatrix`] and the [`PatternVector`], before calling +/// [`algorithm_u`] recursively with the specialised structures. fn algorithm_u_constructor( state: &mut CheckState, context: &CheckContext, @@ -100,36 +107,49 @@ fn algorithm_u_constructor( where Q: ExternalQueries, { - let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + let specialised_matrix = specialise_matrix(state, &constructor, matrix); - let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) else { + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - algorithm_u(state, context, &specialized_matrix, &specialized_vector) + algorithm_u(state, context, &specialised_matrix, &specialised_vector) } +/// Induction 2 +/// +/// This function collects all constructor references from the first column of +/// the matrix into a collection called the sigma. +/// +/// If the sigma is complete, for each constructor in the sigma, we specialise +/// the pattern matrix and pattern vector against it. Then, we recursively call +/// [`algorithm_u`] against the specialised structures. The pattern vector is +/// useful if any specialised pattern vector is useful against its specialised +/// pattern matrix. +/// +/// If the sigma is incomplete, we recursively call [`algorithm_u`] against the +/// [`default_matrix`] of the pattern matrix and the tail of the pattern vector. fn algorithm_u_wildcard( state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - first_type: TypeId, + t: TypeId, ) -> QueryResult where Q: ExternalQueries, { - let sigma = collect_sigma(state, context, matrix, first_type)?; + let sigma = collect_sigma(state, context, matrix, t)?; let complete = sigma_is_complete(context, &sigma)?; if complete { - algorithm_u_complete(state, context, matrix, vector, sigma) + algorithm_u_wildcard_complete(state, context, matrix, vector, sigma) } else { algorithm_u_wildcard_incomplete(state, context, matrix, vector) } } -fn algorithm_u_complete( +fn algorithm_u_wildcard_complete( state: &mut CheckState, context: &CheckContext, matrix: &PatternMatrix, @@ -140,14 +160,13 @@ where Q: ExternalQueries, { for constructor in sigma.constructors { - let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + let specialised_matrix = specialise_matrix(state, &constructor, matrix); - let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) - else { + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - if algorithm_u(state, context, &specialized_matrix, &specialized_vector)? { + if algorithm_u(state, context, &specialised_matrix, &specialised_vector)? { return Ok(true); } } @@ -195,7 +214,8 @@ where /// So... what exactly are witnesses? In the paper, these are defined as /// 'value vectors' that are known not to be matched against the pattern /// matrix but are instantiations of the pattern vector. In our implementation, -/// these witnesses are patterns not covered yet by the matrix. +/// these witnesses are pattern vectors that denote values not yet covered by +/// the matrix. /// /// The [`algorithm_m_wildcard`] induction is prolific for producing these /// these witnesses as it compares the constructors that appear in the @@ -250,20 +270,20 @@ fn algorithm_m_constructor( matrix: &PatternMatrix, vector: &PatternVector, constructor: Constructor, - first_type: TypeId, + t: TypeId, ) -> QueryResult>> where Q: ExternalQueries, { let arity = constructor.fields.len(); - let specialized_matrix = specialise_matrix(state, context, &constructor, matrix); + let specialised_matrix = specialise_matrix(state, &constructor, matrix); - let Some(specialized_vector) = specialise_vector(state, context, &constructor, vector) else { + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; - let witnesses = algorithm_m(state, context, &specialized_matrix, &specialized_vector)?; + let witnesses = algorithm_m(state, context, &specialised_matrix, &specialised_vector)?; let Some(witnesses) = witnesses else { return Ok(None); @@ -280,7 +300,7 @@ where }, }; - let constructor = state.allocate_pattern(pattern, first_type); + let constructor = state.allocate_pattern(pattern, t); let tail_columns = tail_columns.iter().copied(); iter::once(constructor).chain(tail_columns).collect() @@ -319,17 +339,17 @@ fn algorithm_m_wildcard( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - first_type: TypeId, + t: TypeId, ) -> QueryResult>> where Q: ExternalQueries, { - let sigma = collect_sigma(state, context, matrix, first_type)?; + let sigma = collect_sigma(state, context, matrix, t)?; let complete = sigma_is_complete(context, &sigma)?; if complete { - algorithm_m_wildcard_complete(state, context, matrix, vector, first_type, &sigma) + algorithm_m_wildcard_complete(state, context, matrix, vector, t, &sigma) } else { - algorithm_m_wildcard_incomplete(state, context, matrix, vector, first_type, &sigma) + algorithm_m_wildcard_incomplete(state, context, matrix, vector, t, &sigma) } } @@ -338,7 +358,7 @@ fn algorithm_m_wildcard_complete( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - first_type: TypeId, + t: TypeId, sigma: &Sigma, ) -> QueryResult>> where @@ -349,15 +369,14 @@ where for constructor in &sigma.constructors { let arity = constructor.fields.len(); - let specialized_matrix = specialise_matrix(state, context, constructor, matrix); + let specialised_matrix = specialise_matrix(state, constructor, matrix); - let Some(specialized_vector) = specialise_vector(state, context, constructor, vector) - else { + let Some(specialised_vector) = specialise_vector(state, constructor, vector) else { unreachable!("invariant violated: vector contains constructor"); }; if let Some(witnesses) = - algorithm_m(state, context, &specialized_matrix, &specialized_vector)? + algorithm_m(state, context, &specialised_matrix, &specialised_vector)? { for witness in witnesses { let (argument_columns, tail_columns) = witness.split_at(arity); @@ -370,7 +389,7 @@ where }, }; - let constructor = state.allocate_pattern(pattern, first_type); + let constructor = state.allocate_pattern(pattern, t); let tail_columns = tail_columns.iter().copied(); let witnesses = iter::once(constructor).chain(tail_columns).collect(); @@ -387,7 +406,7 @@ fn algorithm_m_wildcard_incomplete( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - first_type: TypeId, + t: TypeId, sigma: &Sigma, ) -> QueryResult>> where @@ -413,9 +432,9 @@ where }, }; - state.allocate_pattern(pattern, first_type) + state.allocate_pattern(pattern, t) } else { - state.allocate_wildcard(first_type) + state.allocate_wildcard(t) }; let witness = witnesses @@ -431,7 +450,7 @@ fn algorithm_m_other( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - first_type: TypeId, + t: TypeId, ) -> QueryResult>> where Q: ExternalQueries, @@ -447,36 +466,36 @@ where }; // Prefix with wildcard - let head = state.allocate_wildcard(first_type); + let head = state.allocate_wildcard(t); Ok(Some(witnesses.into_iter().map(|w| iter::once(head).chain(w).collect()).collect())) } /// Specialises a [`PatternMatrix`] given a [`Constructor`]. -fn specialise_matrix( +/// +/// See documentation below for [`specialise_vector`]. +fn specialise_matrix( state: &mut CheckState, - context: &CheckContext, expected: &Constructor, matrix: &PatternMatrix, -) -> PatternMatrix -where - Q: ExternalQueries, -{ - let matrix = matrix.iter().filter_map(|row| specialise_vector(state, context, expected, row)); - matrix.collect() +) -> PatternMatrix { + matrix.iter().filter_map(|row| specialise_vector(state, expected, row)).collect() } /// Specialises a [`PatternVector`] given a [`Constructor`]. -fn specialise_vector( +/// +/// Specialisation takes a pattern vector and applies the following rules: +/// 1. If the first column is a wildcard, it expands it to `n` wildcards +/// where `n` is the arity of the expected [`Constructor`]. +/// 2. It returns `None` for constructors that are not the expected +/// [`Constructor`], which excludes them from the specialised matrix. +/// For example, a pattern vector specialised on `Just` removes `Nothing`. +/// 3. For matching constructors, it 'splats' the fields, effectively turning +/// a pattern vector like `[Just _]` into `[_]` or `[Nothing]` into `[]`. +fn specialise_vector( state: &mut CheckState, - context: &CheckContext, expected: &Constructor, vector: &PatternVector, -) -> Option -where - Q: ExternalQueries, -{ - let _ = context; - +) -> Option { let [first_column, ref tail_columns @ ..] = vector[..] else { unreachable!("invariant violated: specialise_vector processed empty row"); }; From e311dea4552cc99cacbd77c5931de326eaae956d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Feb 2026 23:21:28 +0800 Subject: [PATCH 082/386] Consider literal patterns as constructor patterns This commit implements proper handling for literal patterns, which are effectively a special case of constructor patterns. For literals like Boolean, the domain is reasonably bounded and we can generate useful witnesses to yield useful warnings similar to user-defined data types. For literals like String where the domain is practically infinite, the sigma is always incomplete, which means we construct wildcard-based witnesses for pattern matrices that do not have a fallback. --- .../checking/src/algorithm/exhaustiveness.rs | 411 +++++++++++++----- .../src/algorithm/exhaustiveness/convert.rs | 37 +- .../src/algorithm/exhaustiveness/pretty.rs | 64 +-- compiler-core/checking/src/algorithm/state.rs | 13 +- .../259_exhaustive_boolean_partial/Main.purs | 25 ++ .../259_exhaustive_boolean_partial/Main.snap | 30 ++ .../260_exhaustive_integer_partial/Main.purs | 14 + .../260_exhaustive_integer_partial/Main.snap | 23 + .../261_exhaustive_number_partial/Main.purs | 14 + .../261_exhaustive_number_partial/Main.snap | 23 + .../262_exhaustive_char_partial/Main.purs | 14 + .../262_exhaustive_char_partial/Main.snap | 23 + .../263_exhaustive_string_partial/Main.purs | 14 + .../263_exhaustive_string_partial/Main.snap | 23 + tests-integration/tests/checking/generated.rs | 10 + 15 files changed, 578 insertions(+), 160 deletions(-) create mode 100644 tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap create mode 100644 tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap create mode 100644 tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap create mode 100644 tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap create mode 100644 tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index bf426902..96cf32ae 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -23,21 +23,9 @@ pub struct Pattern { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum PatternKind { Wildcard, - Boolean(bool), - Char(char), - String(SmolStr), - Integer(i32), - Number(bool, SmolStr), Array { elements: Vec }, Record { elements: Vec }, - Constructor { constructor: Constructor }, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Constructor { - pub file_id: FileId, - pub item_id: TermItemId, - pub fields: Vec, + Constructor { constructor: PatternConstructor }, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -46,6 +34,114 @@ pub enum RecordElement { Pun(SmolStr), } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PatternConstructor { + DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Boolean(bool), + Integer(i32), + Number(bool, SmolStr), + String(SmolStr), + Char(char), +} + +impl PatternConstructor { + /// Returns the arity of this pattern constructor. + /// + /// Only [`PatternConstructor::DataConstructor`] has a non-zero arity. + pub fn arity(&self) -> usize { + if let PatternConstructor::DataConstructor { fields, .. } = self { fields.len() } else { 0 } + } + + /// Returns the fields of this pattern constructor. + /// + /// Only [`PatternConstructor::DataConstructor`] should have fields. + pub fn fields(&self) -> &[PatternId] { + if let PatternConstructor::DataConstructor { fields, .. } = self { fields } else { &[] } + } + + /// Checks if a pattern constructor matches another. + /// + /// This is used during the specialisation algorithm to determine if a + /// pattern row should be included in the specialised pattern matrix. + pub fn matches(&self, other: &PatternConstructor) -> bool { + match (self, other) { + ( + PatternConstructor::DataConstructor { file_id: f1, item_id: i1, .. }, + PatternConstructor::DataConstructor { file_id: f2, item_id: i2, .. }, + ) => f1 == f2 && i1 == i2, + (PatternConstructor::Boolean(b1), PatternConstructor::Boolean(b2)) => b1 == b2, + (PatternConstructor::Integer(i1), PatternConstructor::Integer(i2)) => i1 == i2, + (PatternConstructor::Number(n1, v1), PatternConstructor::Number(n2, v2)) => { + n1 == n2 && v1 == v2 + } + (PatternConstructor::String(s1), PatternConstructor::String(s2)) => s1 == s2, + (PatternConstructor::Char(c1), PatternConstructor::Char(c2)) => c1 == c2, + _ => false, + } + } + + /// Reconstructs a [`PatternConstructor`] with the given fields. + /// + /// For [`PatternConstructor::DataConstructor`], this function overrides the + /// fields. Otherwise, the fields must be empty as enforced by an assertion. + /// + /// This algorithm is used in [`algorithm_m`] to replace the fields of the + /// pattern constructor we're specialising on with the fields generated by + /// the witnesses. + pub fn reconstruct(&self, fields: &[PatternId]) -> PatternConstructor { + match *self { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + let fields = fields.to_vec(); + PatternConstructor::DataConstructor { file_id, item_id, fields } + } + PatternConstructor::Boolean(b) => { + assert!(fields.is_empty(), "Boolean constructor has arity 0"); + PatternConstructor::Boolean(b) + } + PatternConstructor::Integer(i) => { + assert!(fields.is_empty(), "Integer constructor has arity 0"); + PatternConstructor::Integer(i) + } + PatternConstructor::Number(negative, ref n) => { + assert!(fields.is_empty(), "Number constructor has arity 0"); + PatternConstructor::Number(negative, SmolStr::clone(n)) + } + PatternConstructor::String(ref s) => { + assert!(fields.is_empty(), "String constructor has arity 0"); + PatternConstructor::String(SmolStr::clone(s)) + } + PatternConstructor::Char(c) => { + assert!(fields.is_empty(), "Char constructor has arity 0"); + PatternConstructor::Char(c) + } + } + } +} + +impl MissingConstructor { + /// Constructs a witness pattern for this missing constructor. + pub fn construct_missing_witness(&self, state: &mut CheckState, t: TypeId) -> PatternId { + match *self { + MissingConstructor::DataConstructor { file_id, item_id, ref fields } => { + let fields = fields + .iter() + .map(|&field_type| state.allocate_wildcard(field_type)) + .collect_vec(); + + let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields }; + let pattern = PatternKind::Constructor { constructor }; + + state.allocate_pattern(pattern, t) + } + MissingConstructor::Boolean(b) => { + let constructor = PatternConstructor::Boolean(b); + let pattern = PatternKind::Constructor { constructor }; + state.allocate_pattern(pattern, t) + } + } + } +} + pub type PatternId = interner::Id; pub type PatternStorage = interner::Interner; @@ -88,13 +184,15 @@ where PatternKind::Wildcard => { algorithm_u_wildcard(state, context, matrix, vector, first_pattern.t) } - _ => algorithm_u_other(state, context, matrix, vector), + PatternKind::Array { .. } | PatternKind::Record { .. } => { + algorithm_u_other(state, context, matrix, vector) + } } } /// Induction 1 /// -/// This function uses specialisation to spread the provided [`Constructor`] +/// This function uses specialisation to spread the provided [`PatternConstructor`] /// over both the [`PatternMatrix`] and the [`PatternVector`], before calling /// [`algorithm_u`] recursively with the specialised structures. fn algorithm_u_constructor( @@ -102,7 +200,7 @@ fn algorithm_u_constructor( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - constructor: Constructor, + constructor: PatternConstructor, ) -> QueryResult where Q: ExternalQueries, @@ -249,13 +347,15 @@ where PatternKind::Wildcard => { algorithm_m_wildcard(state, context, matrix, vector, first_pattern.t) } - _ => algorithm_m_other(state, context, matrix, vector, first_pattern.t), + PatternKind::Array { .. } | PatternKind::Record { .. } => { + algorithm_m_other(state, context, matrix, vector, first_pattern.t) + } } } /// Induction 1 /// -/// This function uses specialisation to spread the provided [`Constructor`] +/// This function uses specialisation to spread the provided [`PatternConstructor`] /// over both the [`PatternMatrix`] and the [`PatternVector`], before calling /// [`algorithm_m`] recursively with the specialised structures. /// @@ -269,13 +369,13 @@ fn algorithm_m_constructor( context: &CheckContext, matrix: &PatternMatrix, vector: &PatternVector, - constructor: Constructor, + constructor: PatternConstructor, t: TypeId, ) -> QueryResult>> where Q: ExternalQueries, { - let arity = constructor.fields.len(); + let arity = constructor.arity(); let specialised_matrix = specialise_matrix(state, &constructor, matrix); @@ -292,18 +392,11 @@ where let witnesses = witnesses.into_iter().map(|witness| { let (argument_columns, tail_columns) = witness.split_at(arity); - let pattern = PatternKind::Constructor { - constructor: Constructor { - file_id: constructor.file_id, - item_id: constructor.item_id, - fields: argument_columns.to_vec(), - }, - }; - - let constructor = state.allocate_pattern(pattern, t); + let constructor = constructor.reconstruct(argument_columns); + let constructor_id = state.allocate_constructor(constructor, t); let tail_columns = tail_columns.iter().copied(); - iter::once(constructor).chain(tail_columns).collect() + iter::once(constructor_id).chain(tail_columns).collect() }); let witnesses = witnesses.collect(); @@ -367,7 +460,7 @@ where let mut all_witnesses = vec![]; for constructor in &sigma.constructors { - let arity = constructor.fields.len(); + let arity = constructor.arity(); let specialised_matrix = specialise_matrix(state, constructor, matrix); @@ -381,18 +474,11 @@ where for witness in witnesses { let (argument_columns, tail_columns) = witness.split_at(arity); - let pattern = PatternKind::Constructor { - constructor: Constructor { - file_id: constructor.file_id, - item_id: constructor.item_id, - fields: argument_columns.to_vec(), - }, - }; - - let constructor = state.allocate_pattern(pattern, t); + let constructor = constructor.reconstruct(argument_columns); + let constructor_id = state.allocate_constructor(constructor, t); let tail_columns = tail_columns.iter().copied(); - let witnesses = iter::once(constructor).chain(tail_columns).collect(); + let witnesses = iter::once(constructor_id).chain(tail_columns).collect(); all_witnesses.push(witnesses); } } @@ -422,17 +508,7 @@ where }; let first_column = if let Some(constructor) = sigma.missing.first() { - let fields = constructor.fields.iter().map(|&t| state.allocate_wildcard(t)).collect_vec(); - - let pattern = PatternKind::Constructor { - constructor: Constructor { - file_id: constructor.file_id, - item_id: constructor.item_id, - fields, - }, - }; - - state.allocate_pattern(pattern, t) + constructor.construct_missing_witness(state, t) } else { state.allocate_wildcard(t) }; @@ -470,30 +546,32 @@ where Ok(Some(witnesses.into_iter().map(|w| iter::once(head).chain(w).collect()).collect())) } -/// Specialises a [`PatternMatrix`] given a [`Constructor`]. +/// Specialises a [`PatternMatrix`] given a [`PatternConstructor`]. /// /// See documentation below for [`specialise_vector`]. fn specialise_matrix( state: &mut CheckState, - expected: &Constructor, + expected: &PatternConstructor, matrix: &PatternMatrix, ) -> PatternMatrix { matrix.iter().filter_map(|row| specialise_vector(state, expected, row)).collect() } -/// Specialises a [`PatternVector`] given a [`Constructor`]. +/// Specialises a [`PatternVector`] given a [`PatternConstructor`]. /// /// Specialisation takes a pattern vector and applies the following rules: /// 1. If the first column is a wildcard, it expands it to `n` wildcards -/// where `n` is the arity of the expected [`Constructor`]. +/// where `n` is the arity of the expected [`PatternConstructor`]. +/// For non-ADT constructors, arity is 0 (no expansion needed). /// 2. It returns `None` for constructors that are not the expected -/// [`Constructor`], which excludes them from the specialised matrix. +/// [`PatternConstructor`], which excludes them from the specialised matrix. /// For example, a pattern vector specialised on `Just` removes `Nothing`. /// 3. For matching constructors, it 'splats' the fields, effectively turning /// a pattern vector like `[Just _]` into `[_]` or `[Nothing]` into `[]`. +/// For non-ADT constructors, arity is 0 so nothing is splatted. fn specialise_vector( state: &mut CheckState, - expected: &Constructor, + expected: &PatternConstructor, vector: &PatternVector, ) -> Option { let [first_column, ref tail_columns @ ..] = vector[..] else { @@ -503,23 +581,34 @@ fn specialise_vector( let first_column = &state.patterns[first_column]; if let PatternKind::Wildcard = first_column.kind { - let wildcards = expected.fields.iter().map(|&pattern| { - let t = state.patterns[pattern].t; - state.allocate_wildcard(t) - }); - let tail_columns = tail_columns.iter().copied(); - return Some(iter::chain(wildcards, tail_columns).collect()); + if let PatternConstructor::DataConstructor { fields, .. } = expected { + let wildcards = fields.iter().map(|&pattern_id| { + let t = state.patterns[pattern_id].t; + state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } else { + return Some(tail_columns.to_vec()); + } } let PatternKind::Constructor { constructor } = &first_column.kind else { return Some(tail_columns.to_vec()); }; - if (constructor.file_id, constructor.item_id) != (expected.file_id, expected.item_id) { + // Check if constructors match + if !constructor.matches(expected) { return None; } - Some(iter::chain(&constructor.fields, tail_columns).copied().collect()) + // Only DataConstructor has fields to splat + match constructor { + PatternConstructor::DataConstructor { fields, .. } => { + Some(iter::chain(fields, tail_columns).copied().collect()) + } + _ => Some(tail_columns.to_vec()), + } } fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { @@ -536,28 +625,55 @@ fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { filter_map.collect() } -/// Key for identifying a unique constructor (file + term item). -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -struct ConstructorKey(FileId, TermItemId); +/// Key for identifying a unique constructor. +#[derive(Clone, PartialEq, Eq, Hash)] +enum ConstructorKey { + Data(FileId, TermItemId), + Boolean(bool), + Integer(i32), + Number(bool, SmolStr), + String(SmolStr), + Char(char), +} + +impl ConstructorKey { + fn from_pattern_constructor(pc: &PatternConstructor) -> Self { + match pc { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + ConstructorKey::Data(*file_id, *item_id) + } + PatternConstructor::Boolean(b) => ConstructorKey::Boolean(*b), + PatternConstructor::Integer(i) => ConstructorKey::Integer(*i), + PatternConstructor::Number(negative, n) => { + let n = SmolStr::clone(n); + ConstructorKey::Number(*negative, n) + } + PatternConstructor::String(s) => { + let s = SmolStr::clone(s); + ConstructorKey::String(s) + } + PatternConstructor::Char(c) => ConstructorKey::Char(*c), + } + } +} #[derive(Clone, Debug)] struct Sigma { - constructors: Vec, + constructors: Vec, missing: Vec, } #[derive(Clone, Debug)] -struct MissingConstructor { - file_id: FileId, - item_id: TermItemId, - fields: Vec, +enum MissingConstructor { + DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Boolean(bool), } -/// Extracts the set of constructors (sigma) from the first column of the matrix. +/// Extracts the sigma, a set of constructors from the first column of the matrix. /// /// Returns a list of unique constructors seen in the first column, keeping one -/// representative `Constructor` per distinct constructor id. Non-constructor -/// patterns (wildcards, literals, etc.) are ignored. +/// representative [`PatternConstructor`] per distinct constructor. Other patterns +/// like wildcards, records, and arrays are ignored for now. fn collect_sigma( state: &mut CheckState, context: &CheckContext, @@ -576,9 +692,9 @@ where }; let pattern = &state.patterns[first_column]; if let PatternKind::Constructor { constructor } = &pattern.kind { - let key = ConstructorKey(constructor.file_id, constructor.item_id); + let key = ConstructorKey::from_pattern_constructor(constructor); if seen.insert(key) { - constructors.push(Constructor::clone(constructor)); + constructors.push(constructor.clone()); } } } @@ -589,67 +705,130 @@ where /// Checks whether the set of constructors (sigma) is complete for the scrutinee type. /// -/// A sigma is complete if it contains all constructors of the data type. If we can't -/// determine the type or its constructors, we conservatively return false (incomplete). +/// A sigma is complete if it contains all constructors of the data type. +/// For Boolean, both true and false must be present. +/// For other literal constructors (Integer, Number, String, Char), sigma is never complete +/// (infinite domains). +/// If we can't determine the type or its constructors, we conservatively return false. fn sigma_is_complete(context: &CheckContext, sigma: &Sigma) -> QueryResult where Q: ExternalQueries, { - // Empty sigma is never complete (we need at least one constructor to determine the type) + // Empty sigma is never complete let Some(first) = sigma.constructors.first() else { return Ok(false); }; - // Get the indexed module for the constructor's file - let indexed = context.queries.indexed(first.file_id)?; - - // Find the type this constructor belongs to - let Some(type_item_id) = indexed.pairs.constructor_type(first.item_id) else { - return Ok(false); - }; - - // Get all constructors for this type - let all_constructors: FxHashSet = - indexed.pairs.data_constructors(type_item_id).collect(); - - // Check if sigma covers all constructors - let sigma_terms: FxHashSet = sigma.constructors.iter().map(|c| c.item_id).collect(); - - Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) + match first { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + // Get the indexed module for the constructor's file + let indexed = context.queries.indexed(*file_id)?; + + // Find the type this constructor belongs to + let Some(type_item_id) = indexed.pairs.constructor_type(*item_id) else { + return Ok(false); + }; + + // Get all constructors for this type + let all_constructors: FxHashSet = + indexed.pairs.data_constructors(type_item_id).collect(); + + // Check if sigma covers all constructors + let sigma_terms: FxHashSet = sigma + .constructors + .iter() + .filter_map(|c| match c { + PatternConstructor::DataConstructor { item_id, .. } => Some(*item_id), + _ => None, + }) + .collect(); + + Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) + } + PatternConstructor::Boolean(_) => { + // Boolean is complete when both true and false are present + let has_true = + sigma.constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(true))); + let has_false = + sigma.constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(false))); + Ok(has_true && has_false) + } + // Other literal constructors have infinite domains, so they're never complete + PatternConstructor::Integer(_) + | PatternConstructor::Number(_, _) + | PatternConstructor::String(_) + | PatternConstructor::Char(_) => Ok(false), + } } fn collect_missing_constructors( state: &mut CheckState, context: &CheckContext, scrutinee_type: TypeId, - constructors: &[Constructor], + constructors: &[PatternConstructor], ) -> QueryResult> where Q: ExternalQueries, { - let Some(constructor) = constructors.first() else { - return Ok(vec![]); - }; - - let indexed = context.queries.indexed(constructor.file_id)?; - - let Some(type_item_id) = indexed.pairs.constructor_type(constructor.item_id) else { + let Some(first_constructor) = constructors.first() else { return Ok(vec![]); }; - let sigma: FxHashSet = constructors.iter().map(|c| c.item_id).collect(); - let arguments = toolkit::extract_all_applications(state, scrutinee_type); + match first_constructor { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + let indexed = context.queries.indexed(*file_id)?; + + let Some(type_item_id) = indexed.pairs.constructor_type(*item_id) else { + return Ok(vec![]); + }; + + let sigma: FxHashSet = constructors + .iter() + .filter_map(|c| match c { + PatternConstructor::DataConstructor { item_id, .. } => Some(*item_id), + _ => None, + }) + .collect(); + let arguments = toolkit::extract_all_applications(state, scrutinee_type); + + let mut missing = vec![]; + for missing_item_id in indexed.pairs.data_constructors(type_item_id) { + if !sigma.contains(&missing_item_id) { + let fields = constructor_field_types( + state, + context, + *file_id, + missing_item_id, + &arguments, + )?; + missing.push(MissingConstructor::DataConstructor { + file_id: *file_id, + item_id: missing_item_id, + fields, + }); + } + } - let mut missing = vec![]; - for item_id in indexed.pairs.data_constructors(type_item_id) { - let file_id = constructor.file_id; - if !sigma.contains(&item_id) { - let fields = constructor_field_types(state, context, file_id, item_id, &arguments)?; - missing.push(MissingConstructor { file_id, item_id, fields }); + Ok(missing) } + PatternConstructor::Boolean(_) => { + // Check which boolean values are missing + let has_true = + constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(true))); + let has_false = + constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(false))); + let mut missing = vec![]; + if !has_true { + missing.push(MissingConstructor::Boolean(true)); + } + if !has_false { + missing.push(MissingConstructor::Boolean(false)); + } + Ok(missing) + } + // Other literal constructors have infinite domains, so we don't report specific missing values + _ => Ok(vec![]), } - - Ok(missing) } fn constructor_field_types( diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index 533dc188..8d287bbe 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -7,7 +7,7 @@ use lowering::{BinderId, TermOperatorId}; use smol_str::SmolStr; use sugar::OperatorTree; -use crate::algorithm::exhaustiveness::{Constructor, PatternId, PatternKind, RecordElement}; +use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId, PatternKind, RecordElement}; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; use crate::{ExternalQueries, TypeId}; @@ -36,13 +36,16 @@ where convert_operator_chain_binder(state, context, id, t) } lowering::BinderKind::Integer { value } => match value { - Some(v) => state.allocate_pattern(PatternKind::Integer(*v), t), + Some(v) => { + let constructor = PatternConstructor::Integer(*v); + state.allocate_constructor(constructor, t) + } None => state.allocate_wildcard(t), }, lowering::BinderKind::Number { negative, value } => { if let Some(value) = value { - let kind = PatternKind::Number(*negative, SmolStr::clone(value)); - state.allocate_pattern(kind, t) + let constructor = PatternConstructor::Number(*negative, SmolStr::clone(value)); + state.allocate_constructor(constructor, t) } else { state.allocate_wildcard(t) } @@ -58,18 +61,22 @@ where lowering::BinderKind::Wildcard => state.allocate_wildcard(t), lowering::BinderKind::String { value, .. } => { if let Some(value) = value { - let kind = PatternKind::String(SmolStr::clone(value)); - state.allocate_pattern(kind, t) + let constructor = PatternConstructor::String(SmolStr::clone(value)); + state.allocate_constructor(constructor, t) } else { state.allocate_wildcard(t) } } lowering::BinderKind::Char { value } => match value { - Some(v) => state.allocate_pattern(PatternKind::Char(*v), t), + Some(v) => { + let constructor = PatternConstructor::Char(*v); + state.allocate_constructor(constructor, t) + } None => state.allocate_wildcard(t), }, lowering::BinderKind::Boolean { boolean } => { - state.allocate_pattern(PatternKind::Boolean(*boolean), t) + let constructor = PatternConstructor::Boolean(*boolean); + state.allocate_constructor(constructor, t) } lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), @@ -142,15 +149,15 @@ fn convert_constructor_binder( where Q: ExternalQueries, { - let Some((file_id, item_id)) = resolution else { + let Some((file_id, item_id)) = *resolution else { return state.allocate_wildcard(t); }; let fields = arguments.iter().map(|argument| convert_binder(state, context, *argument)).collect_vec(); - let constructor = Constructor { file_id: *file_id, item_id: *item_id, fields }; - state.allocate_pattern(PatternKind::Constructor { constructor }, t) + let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields }; + state.allocate_constructor(constructor, t) } fn convert_operator_chain_binder( @@ -216,6 +223,10 @@ where let left_pattern = convert_operator_tree(state, context, left_tree, left); let right_pattern = convert_operator_tree(state, context, right_tree, right); - let constructor = Constructor { file_id, item_id, fields: vec![left_pattern, right_pattern] }; - state.allocate_pattern(PatternKind::Constructor { constructor }, result) + let constructor = PatternConstructor::DataConstructor { + file_id, + item_id, + fields: vec![left_pattern, right_pattern], + }; + state.allocate_constructor(constructor, result) } diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index 20c5dd4a..93c0dcfe 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{ - Constructor, PatternId, PatternKind, RecordElement, WitnessVector, + PatternConstructor, PatternId, PatternKind, RecordElement, WitnessVector, }; use crate::algorithm::state::{CheckContext, CheckState}; @@ -28,17 +28,6 @@ where let pattern = &state.patterns[id]; match &pattern.kind { PatternKind::Wildcard => "_".to_string(), - PatternKind::Boolean(b) => b.to_string(), - PatternKind::Char(c) => format!("'{c}'"), - PatternKind::String(s) => format!("\"{s}\""), - PatternKind::Integer(i) => i.to_string(), - PatternKind::Number(negative, n) => { - if *negative { - format!("-{n}") - } else { - n.to_string() - } - } PatternKind::Array { elements } => { let mut elements = elements.iter().map(|&e| pretty_pattern(context, state, e)); format!("[{}]", elements.join(", ")) @@ -60,31 +49,46 @@ where fn pretty_constructor( context: &CheckContext, state: &CheckState, - constructor: &Constructor, + constructor: &PatternConstructor, ) -> String where Q: ExternalQueries, { - let name = lookup_constructor_name(context, constructor.file_id, constructor.item_id) - .unwrap_or_else(|| "".to_string()); + match constructor { + PatternConstructor::DataConstructor { file_id, item_id, fields } => { + let name = lookup_constructor_name(context, *file_id, *item_id) + .unwrap_or_else(|| "".to_string()); - if constructor.fields.is_empty() { - return name; - } + if fields.is_empty() { + return name; + } - let mut fields = constructor.fields.iter().map(|&id| { - let rendered = pretty_pattern(context, state, id); - let pattern = &state.patterns[id]; - if let PatternKind::Constructor { constructor } = &pattern.kind - && !constructor.fields.is_empty() - { - format!("({rendered})") - } else { - rendered - } - }); + let mut field_strings = fields.iter().map(|&id| { + let rendered = pretty_pattern(context, state, id); + let pattern = &state.patterns[id]; + if let PatternKind::Constructor { constructor } = &pattern.kind + && !constructor.fields().is_empty() + { + format!("({rendered})") + } else { + rendered + } + }); - format!("{} {}", name, fields.join(" ")) + format!("{} {}", name, field_strings.join(" ")) + } + PatternConstructor::Boolean(b) => b.to_string(), + PatternConstructor::Char(c) => format!("'{c}'"), + PatternConstructor::String(s) => format!("\"{s}\""), + PatternConstructor::Integer(i) => i.to_string(), + PatternConstructor::Number(negative, n) => { + if *negative { + format!("-{n}") + } else { + n.to_string() + } + } + } } fn lookup_constructor_name( diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index a06bd436..fad77fba 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -19,7 +19,9 @@ use smol_str::ToSmolStr; use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; -use crate::algorithm::exhaustiveness::{Pattern, PatternId, PatternKind, PatternStorage}; +use crate::algorithm::exhaustiveness::{ + Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, +}; use crate::algorithm::{constraint, transfer}; use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; use crate::error::{CheckError, ErrorKind, ErrorStep}; @@ -1128,6 +1130,15 @@ impl CheckState { self.patterns.intern(pattern) } + pub fn allocate_constructor( + &mut self, + constructor: PatternConstructor, + t: TypeId, + ) -> PatternId { + let kind = PatternKind::Constructor { constructor }; + self.allocate_pattern(kind, t) + } + pub fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { self.allocate_pattern(PatternKind::Wildcard, t) } diff --git a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.purs b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.purs new file mode 100644 index 00000000..0ff0c5fa --- /dev/null +++ b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.purs @@ -0,0 +1,25 @@ +module Main where + +testPartialTrue :: Boolean -> Int +testPartialTrue b = case b of + true -> 1 + +testPartialFalse :: Boolean -> Int +testPartialFalse b = case b of + false -> 0 + +testExhaustive :: Boolean -> Int +testExhaustive b = case b of + true -> 1 + false -> 0 + +testNestedPartial :: Boolean -> Boolean -> Int +testNestedPartial x y = case x of + true -> case y of + true -> 1 + false -> 0 + +testWildcardBoolean :: Boolean -> Int +testWildcardBoolean b = case b of + true -> 1 + _ -> 0 diff --git a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap new file mode 100644 index 00000000..1cf8b5d5 --- /dev/null +++ b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testPartialTrue :: Boolean -> Int +testPartialFalse :: Boolean -> Int +testExhaustive :: Boolean -> Int +testNestedPartial :: Boolean -> Boolean -> Int +testWildcardBoolean :: Boolean -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: false + --> 4:21..5:12 + | +4 | testPartialTrue b = case b of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: true + --> 8:22..9:13 + | +8 | testPartialFalse b = case b of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: false + --> 18:11..19:14 + | +18 | true -> case y of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.purs b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.purs new file mode 100644 index 00000000..d6315865 --- /dev/null +++ b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.purs @@ -0,0 +1,14 @@ +module Main where + +testPartialZero :: Int -> Int +testPartialZero n = case n of + 0 -> 1 + +testPartialOne :: Int -> Int +testPartialOne n = case n of + 1 -> 1 + +testWildcard :: Int -> Int +testWildcard n = case n of + 0 -> 1 + _ -> 0 diff --git a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap new file mode 100644 index 00000000..15753010 --- /dev/null +++ b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testPartialZero :: Int -> Int +testPartialOne :: Int -> Int +testWildcard :: Int -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 4:21..5:9 + | +4 | testPartialZero n = case n of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:20..9:9 + | +8 | testPartialOne n = case n of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.purs b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.purs new file mode 100644 index 00000000..390e63c4 --- /dev/null +++ b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.purs @@ -0,0 +1,14 @@ +module Main where + +testPartialZero :: Number -> Int +testPartialZero n = case n of + 0.0 -> 1 + +testPartialOneFive :: Number -> Int +testPartialOneFive n = case n of + 1.5 -> 1 + +testWildcard :: Number -> Int +testWildcard n = case n of + 0.0 -> 1 + _ -> 0 diff --git a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap new file mode 100644 index 00000000..20041b4d --- /dev/null +++ b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testPartialZero :: Number -> Int +testPartialOneFive :: Number -> Int +testWildcard :: Number -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 4:21..5:11 + | +4 | testPartialZero n = case n of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:24..9:11 + | +8 | testPartialOneFive n = case n of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.purs b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.purs new file mode 100644 index 00000000..f5b8628c --- /dev/null +++ b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.purs @@ -0,0 +1,14 @@ +module Main where + +testPartialA :: Char -> Int +testPartialA c = case c of + 'a' -> 1 + +testPartialB :: Char -> Int +testPartialB c = case c of + 'b' -> 1 + +testWildcard :: Char -> Int +testWildcard c = case c of + 'a' -> 1 + _ -> 0 diff --git a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap new file mode 100644 index 00000000..964f4470 --- /dev/null +++ b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testPartialA :: Char -> Int +testPartialB :: Char -> Int +testWildcard :: Char -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 4:18..5:11 + | +4 | testPartialA c = case c of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:18..9:11 + | +8 | testPartialB c = case c of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.purs b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.purs new file mode 100644 index 00000000..27defd00 --- /dev/null +++ b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.purs @@ -0,0 +1,14 @@ +module Main where + +testPartialHello :: String -> Int +testPartialHello s = case s of + "hello" -> 1 + +testPartialWorld :: String -> Int +testPartialWorld s = case s of + "world" -> 1 + +testWildcard :: String -> Int +testWildcard s = case s of + "hello" -> 1 + _ -> 0 diff --git a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap new file mode 100644 index 00000000..95f53d87 --- /dev/null +++ b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testPartialHello :: String -> Int +testPartialWorld :: String -> Int +testWildcard :: String -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 4:22..5:15 + | +4 | testPartialHello s = case s of + | ^~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:22..9:15 + | +8 | testPartialWorld s = case s of + | ^~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 14605c5f..09b79dbc 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -531,3 +531,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_257_exhaustive_tuple_main() { run_test("257_exhaustive_tuple", "Main"); } #[rustfmt::skip] #[test] fn test_258_redundant_patterns_main() { run_test("258_redundant_patterns", "Main"); } + +#[rustfmt::skip] #[test] fn test_259_exhaustive_boolean_partial_main() { run_test("259_exhaustive_boolean_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_260_exhaustive_integer_partial_main() { run_test("260_exhaustive_integer_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_261_exhaustive_number_partial_main() { run_test("261_exhaustive_number_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_262_exhaustive_char_partial_main() { run_test("262_exhaustive_char_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_263_exhaustive_string_partial_main() { run_test("263_exhaustive_string_partial", "Main"); } From 65b2855f63cb0d05100ece4c439c2782108a5737 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Feb 2026 00:00:25 +0800 Subject: [PATCH 083/386] Add dedicated error kind for redundant patterns --- compiler-core/checking/src/algorithm/term.rs | 5 ++--- compiler-core/checking/src/error.rs | 3 +++ compiler-core/diagnostics/src/convert.rs | 8 ++++++++ .../fixtures/checking/258_redundant_patterns/Main.snap | 10 +++++----- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 2ddc6065..07d206d7 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -629,9 +629,8 @@ where } for redundant in report.redundant { - let msg = format!("Pattern match has redundant branch: {redundant}"); - let message_id = state.intern_error_message(msg); - state.insert_error(ErrorKind::CustomWarning { message_id }); + let pattern = state.intern_error_message(redundant); + state.insert_error(ErrorKind::RedundantPattern { pattern }); } Ok(inferred_type) diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 2e653810..5daebc6b 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -124,6 +124,9 @@ pub enum ErrorKind { CustomWarning { message_id: TypeErrorMessageId, }, + RedundantPattern { + pattern: TypeErrorMessageId, + }, CustomFailure { message_id: TypeErrorMessageId, }, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 04c0bc48..c2de7538 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -313,6 +313,14 @@ impl ToDiagnostics for CheckError { "CoercibleConstructorNotInScope", "Constructor not in scope for Coercible".to_string(), ), + ErrorKind::RedundantPattern { pattern } => { + let pattern = lookup_message(*pattern); + ( + Severity::Warning, + "RedundantPattern", + format!("Pattern match has redundant branch: {pattern}"), + ) + } ErrorKind::CustomWarning { message_id } => { let msg = lookup_message(*message_id); (Severity::Warning, "CustomWarning", msg.to_string()) diff --git a/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap index 195690a7..daae64e1 100644 --- a/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap +++ b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap @@ -31,27 +31,27 @@ Unit = [] YesNo = [] Diagnostics -warning[CustomWarning]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant branch: _ --> 5:8..8:12 | 5 | unit = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match has redundant branch: Unit +warning[RedundantPattern]: Pattern match has redundant branch: Unit --> 5:8..8:12 | 5 | unit = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match has redundant branch: Yes +warning[RedundantPattern]: Pattern match has redundant branch: Yes --> 12:7..15:11 | 12 | yes = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match has redundant branch: No +warning[RedundantPattern]: Pattern match has redundant branch: No --> 17:6..20:10 | 17 | no = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant branch: _ --> 22:9..25:9 | 22 | yesNo = case _ of From 6738ff5e1278c282f24cec954b85e18b3dbd3330 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Feb 2026 22:19:43 +0800 Subject: [PATCH 084/386] Initial reporting for exhaustiveness in equations --- .../checking/src/algorithm/exhaustiveness.rs | 105 +++++++++++++----- compiler-core/checking/src/algorithm/state.rs | 16 ++- compiler-core/checking/src/algorithm/term.rs | 35 +++--- .../checking/src/algorithm/term_item.rs | 12 +- .../checking/040_pattern_guard/Main.purs | 2 +- .../checking/040_pattern_guard/Main.snap | 23 ++++ .../checking/060_array_binder/Main.snap | 43 +++++++ .../checking/061_record_binder/Main.snap | 42 +++++++ .../checking/224_record_shrinking/Main.snap | 7 ++ .../Main.snap | 5 + .../Main.snap | 5 + .../264_equation_exhaustive_basic/Main.purs | 9 ++ .../264_equation_exhaustive_basic/Main.snap | 34 ++++++ .../checking/265_equation_redundant/Main.purs | 25 +++++ .../checking/265_equation_redundant/Main.snap | 58 ++++++++++ .../checking/266_equation_guarded/Main.purs | 9 ++ .../checking/266_equation_guarded/Main.snap | 22 ++++ .../267_equation_multiple_arguments/Main.purs | 29 +++++ .../267_equation_multiple_arguments/Main.snap | 47 ++++++++ .../268_let_equation_exhaustive/Main.purs | 17 +++ .../268_let_equation_exhaustive/Main.snap | 34 ++++++ .../Main.purs | 7 ++ .../Main.snap | 24 ++++ tests-integration/tests/checking/generated.rs | 12 ++ 24 files changed, 576 insertions(+), 46 deletions(-) create mode 100644 tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.purs create mode 100644 tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap create mode 100644 tests-integration/fixtures/checking/265_equation_redundant/Main.purs create mode 100644 tests-integration/fixtures/checking/265_equation_redundant/Main.snap create mode 100644 tests-integration/fixtures/checking/266_equation_guarded/Main.purs create mode 100644 tests-integration/fixtures/checking/266_equation_guarded/Main.snap create mode 100644 tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.purs create mode 100644 tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap create mode 100644 tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap create mode 100644 tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 96cf32ae..463002c1 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -879,54 +879,102 @@ where } } -pub struct CasePatternReport { +pub struct ExhaustivenessReport { pub missing: Option>, pub redundant: Vec, } -/// Checks case branch usefulness and exhaustiveness against scrutinee types. -/// -/// Returns a report of missing patterns (if any) and redundant branches. -/// Only unconditional branches are counted toward these checks, as pattern -/// guards do not guarantee coverage. pub fn check_case_patterns( state: &mut CheckState, context: &CheckContext, - trunk_types: &[TypeId], + pattern_types: &[TypeId], branches: &[lowering::CaseBranch], -) -> QueryResult +) -> QueryResult +where + Q: ExternalQueries, +{ + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); + } + + let unconditional = collect_unconditional_rows( + state, + context, + branches, + pattern_types, + |branch: &lowering::CaseBranch| (&branch.binders, &branch.guarded_expression), + ); + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + +pub fn check_equation_patterns( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + equations: &[lowering::Equation], +) -> QueryResult where Q: ExternalQueries, { - if trunk_types.is_empty() { - return Ok(CasePatternReport { missing: None, redundant: vec![] }); + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); } - let mut unconditional = vec![]; - for branch in branches { - let is_unconditional = matches!( - &branch.guarded_expression, - Some(lowering::GuardedExpression::Unconditional { .. }) | None - ); - if !is_unconditional { + let unconditional = collect_unconditional_rows( + state, + context, + equations, + pattern_types, + |equation: &lowering::Equation| (&equation.binders, &equation.guarded), + ); + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + +fn collect_unconditional_rows( + state: &mut CheckState, + context: &CheckContext, + items: &[T], + pattern_types: &[TypeId], + to_binders: F, +) -> Vec +where + Q: ExternalQueries, + F: Fn(&T) -> (&[lowering::BinderId], &Option), +{ + let mut pattern_rows = vec![]; + for item in items { + let (binders, guarded) = to_binders(item); + + if !matches!(guarded, Some(lowering::GuardedExpression::Unconditional { .. }) | None) { continue; } - let mut row: PatternVector = vec![]; - for &binder_id in branch.binders.iter() { - row.push(convert::convert_binder(state, context, binder_id)); + let mut pattern_row = vec![]; + for &binder_id in binders { + pattern_row.push(convert::convert_binder(state, context, binder_id)); } - while row.len() < trunk_types.len() { - let t = trunk_types[row.len()]; - row.push(state.allocate_wildcard(t)); - } + let additional = pattern_types.iter().skip(pattern_row.len()); + pattern_row.extend(additional.map(|&t| state.allocate_wildcard(t))); - if !row.is_empty() { - unconditional.push(row); + if !pattern_row.is_empty() { + pattern_rows.push(pattern_row); } } + pattern_rows +} +fn check_exhaustiveness_core( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + unconditional: PatternMatrix, +) -> QueryResult +where + Q: ExternalQueries, +{ let mut redundant = vec![]; let mut matrix = vec![]; for vector in &unconditional { @@ -938,8 +986,7 @@ where } } - let query: PatternVector = trunk_types.iter().map(|&t| state.allocate_wildcard(t)).collect(); - + let query = pattern_types.iter().map(|&t| state.allocate_wildcard(t)).collect(); let witnesses = algorithm_m(state, context, &unconditional, &query)?; let missing = witnesses.map(|witnesses| { witnesses @@ -949,5 +996,5 @@ where .collect() }); - Ok(CasePatternReport { missing, redundant }) + Ok(ExhaustivenessReport { missing, redundant }) } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index fad77fba..5bd10ac0 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -20,7 +20,7 @@ use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; use crate::algorithm::exhaustiveness::{ - Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, + ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, }; use crate::algorithm::{constraint, transfer}; use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; @@ -1006,6 +1006,20 @@ impl CheckState { constraint::solve_constraints(self, context, wanted, given) } + pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { + if let Some(missing) = exhaustiveness.missing { + let message = + format!("Pattern match is not exhaustive. Missing: {}", missing.join(", ")); + let message_id = self.intern_error_message(message); + self.insert_error(ErrorKind::CustomWarning { message_id }); + } + + for redundant in exhaustiveness.redundant { + let pattern = self.intern_error_message(redundant); + self.insert_error(ErrorKind::RedundantPattern { pattern }); + } + } + /// Executes an action with an [`ErrorStep`] in scope. pub fn with_error_step(&mut self, step: ErrorStep, f: F) -> T where diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 07d206d7..c12cbb76 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -38,6 +38,11 @@ where infer_equations_core(state, context, group_type, equations)?; + let (pattern_types, _) = toolkit::extract_function_arguments(state, group_type); + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &pattern_types, equations)?; + state.report_exhaustiveness(exhaustiveness); + let residual_constraints = state.solve_constraints(context)?; Ok((group_type, residual_constraints)) } @@ -191,6 +196,10 @@ where } } + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; + state.report_exhaustiveness(exhaustiveness); + let residual = state.solve_constraints(context)?; for constraint in residual { let constraint = state.render_local_type(context, constraint); @@ -618,20 +627,9 @@ where } } - // Check exhaustiveness/usefulness after binders have been checked (so types are known) - let report = exhaustiveness::check_case_patterns(state, context, &trunk_types, branches)?; - - if let Some(missing) = report.missing { - let patterns = missing.join(", "); - let msg = format!("Pattern match is not exhaustive. Missing: {patterns}"); - let message_id = state.intern_error_message(msg); - state.insert_error(ErrorKind::CustomWarning { message_id }); - } - - for redundant in report.redundant { - let pattern = state.intern_error_message(redundant); - state.insert_error(ErrorKind::RedundantPattern { pattern }); - } + let exhaustiveness = + exhaustiveness::check_case_patterns(state, context, &trunk_types, branches)?; + state.report_exhaustiveness(exhaustiveness); Ok(inferred_type) } @@ -1952,6 +1950,15 @@ where } else { infer_equations_core(state, context, name_type, &name.equations)?; + let (pattern_types, _) = toolkit::extract_function_arguments(state, name_type); + let exhaustiveness = exhaustiveness::check_equation_patterns( + state, + context, + &pattern_types, + &name.equations, + )?; + state.report_exhaustiveness(exhaustiveness); + // No let-generalization: infer equations and solve constraints; // residuals are deferred to parent scope for later error reporting. let residual = state.solve_constraints(context)?; diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index df42d52a..a34eeef0 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -10,7 +10,8 @@ use crate::ExternalQueries; use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; use crate::algorithm::{ - constraint, inspect, kind, quantify, substitute, term, transfer, unification, + constraint, exhaustiveness, inspect, kind, quantify, substitute, term, toolkit, transfer, + unification, }; use crate::core::{Instance, InstanceKind, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -531,6 +532,15 @@ where let inferred_type = state.fresh_unification_type(context); term::infer_equations_core(state, context, inferred_type, &member.equations)?; + let (pattern_types, _) = toolkit::extract_function_arguments(state, specialized_type); + let exhaustiveness = exhaustiveness::check_equation_patterns( + state, + context, + &pattern_types, + &member.equations, + )?; + state.report_exhaustiveness(exhaustiveness); + let matches = unification::subtype(state, context, inferred_type, specialized_type)?; if !matches { let expected = state.render_local_type(context, specialized_type); diff --git a/tests-integration/fixtures/checking/040_pattern_guard/Main.purs b/tests-integration/fixtures/checking/040_pattern_guard/Main.purs index 8e16e385..5b15ea86 100644 --- a/tests-integration/fixtures/checking/040_pattern_guard/Main.purs +++ b/tests-integration/fixtures/checking/040_pattern_guard/Main.purs @@ -8,4 +8,4 @@ bar s | b <- "hello" = b foo' x | c <- 42 = c -bar' s | b <- "hello" = b +bar' s | b <- "hello" = b diff --git a/tests-integration/fixtures/checking/040_pattern_guard/Main.snap b/tests-integration/fixtures/checking/040_pattern_guard/Main.snap index 31b51064..74103fd6 100644 --- a/tests-integration/fixtures/checking/040_pattern_guard/Main.snap +++ b/tests-integration/fixtures/checking/040_pattern_guard/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,3 +10,25 @@ foo' :: forall (t1 :: Type). (t1 :: Type) -> Int bar' :: forall (t4 :: Type). (t4 :: Type) -> String Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:18 + | +3 | foo :: Int -> Int + | ^~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 6:1..6:24 + | +6 | bar :: String -> String + | ^~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 9:1..9:21 + | +9 | foo' x | c <- 42 = c + | ^~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 11:1..11:26 + | +11 | bar' s | b <- "hello" = b + | ^~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/060_array_binder/Main.snap b/tests-integration/fixtures/checking/060_array_binder/Main.snap index fbc354c0..fc3ea2ff 100644 --- a/tests-integration/fixtures/checking/060_array_binder/Main.snap +++ b/tests-integration/fixtures/checking/060_array_binder/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,3 +14,45 @@ nested :: Array (Array Int) -> Int nested' :: forall (t53 :: Type). Array (Array (t53 :: Type)) -> (t53 :: Type) Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:45 + | +3 | test1 :: Array Int -> { x :: Int, y :: Int } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 6:1..6:25 + | +6 | test1' [x, y] = { x, y } + | ^~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:1..8:38 + | +8 | test2 :: forall a. Array a -> Array a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 11:1..11:29 + | +11 | test2' [x, y, z] = [z, y, x] + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 13:1..13:26 + | +13 | test3 :: Array Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 18:1..18:14 + | +18 | test3' [] = 0 + | ^~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 22:1..22:35 + | +22 | nested :: Array (Array Int) -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 26:1..26:18 + | +26 | nested' [[x]] = x + | ^~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/061_record_binder/Main.snap b/tests-integration/fixtures/checking/061_record_binder/Main.snap index 1c4d6738..e272f781 100644 --- a/tests-integration/fixtures/checking/061_record_binder/Main.snap +++ b/tests-integration/fixtures/checking/061_record_binder/Main.snap @@ -23,3 +23,45 @@ nested' :: { inner :: { x :: (t42 :: Type) | (t40 :: Row Type) } | (t41 :: Row Type) } -> (t42 :: Type) Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:39 + | +3 | test1 :: { x :: Int, y :: Int } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 6:1..6:20 + | +6 | test1' { x, y } = x + | ^~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 8:1..8:64 + | +8 | test2 :: { x :: Int, y :: String } -> { x :: Int, y :: String } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 11:1..11:27 + | +11 | test2' { x, y } = { x, y } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 13:1..13:50 + | +13 | test3 :: { name :: String, age :: Int } -> String + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 16:1..16:31 + | +16 | test3' { name: n, age: a } = n + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 18:1..18:43 + | +18 | nested :: { inner :: { x :: Int } } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 21:1..21:29 + | +21 | nested' { inner: { x } } = x + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap index b79528bb..dd64deb9 100644 --- a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap +++ b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap @@ -7,3 +7,10 @@ Terms test :: { a :: Int, b :: Int } -> Int Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:38 + | +3 | test :: { a :: Int, b :: Int } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap index 95be51e9..2800977c 100644 --- a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap +++ b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap @@ -14,3 +14,8 @@ error[AdditionalProperty]: Additional properties not allowed: b, c | 4 | test { a, b, c } = a | ^~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:28 + | +3 | test :: { a :: Int } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap index 8465399f..6f7ca71e 100644 --- a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap +++ b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap @@ -14,3 +14,8 @@ error[AdditionalProperty]: Additional properties not allowed: y | 4 | test { outer: { x, y } } = x | ^~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 3:1..3:41 + | +3 | test :: { outer :: { x :: Int } } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.purs b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.purs new file mode 100644 index 00000000..f7654cfa --- /dev/null +++ b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Maybe a = Just a | Nothing + +test1 :: Maybe Int -> Int +test1 (Just _) = 1 + +test2 :: Maybe Int -> Int +test2 Nothing = 2 diff --git a/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap new file mode 100644 index 00000000..ae9f8bbf --- /dev/null +++ b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test1 :: Maybe Int -> Int +test2 :: Maybe Int -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing + --> 5:1..5:26 + | +5 | test1 :: Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ + --> 8:1..8:26 + | +8 | test2 :: Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/265_equation_redundant/Main.purs b/tests-integration/fixtures/checking/265_equation_redundant/Main.purs new file mode 100644 index 00000000..943cbf1d --- /dev/null +++ b/tests-integration/fixtures/checking/265_equation_redundant/Main.purs @@ -0,0 +1,25 @@ +module Main where + +data Unit = Unit + +unit :: Unit -> Int +unit Unit = 1 +unit _ = 2 +unit Unit = 3 + +data YesNo = Yes | No + +yes :: YesNo -> Int +yes Yes = 1 +yes _ = 2 +yes Yes = 3 + +no :: YesNo -> Int +no Yes = 1 +no _ = 2 +no No = 3 + +yesNo :: YesNo -> Int +yesNo Yes = 1 +yesNo No = 2 +yesNo _ = 3 diff --git a/tests-integration/fixtures/checking/265_equation_redundant/Main.snap b/tests-integration/fixtures/checking/265_equation_redundant/Main.snap new file mode 100644 index 00000000..cb99395a --- /dev/null +++ b/tests-integration/fixtures/checking/265_equation_redundant/Main.snap @@ -0,0 +1,58 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +unit :: Unit -> Int +Yes :: YesNo +No :: YesNo +yes :: YesNo -> Int +no :: YesNo -> Int +yesNo :: YesNo -> Int + +Types +Unit :: Type +YesNo :: Type + +Data +Unit + Quantified = :0 + Kind = :0 + +YesNo + Quantified = :0 + Kind = :0 + + +Roles +Unit = [] +YesNo = [] + +Diagnostics +warning[RedundantPattern]: Pattern match has redundant branch: _ + --> 5:1..5:20 + | +5 | unit :: Unit -> Int + | ^~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: Unit + --> 5:1..5:20 + | +5 | unit :: Unit -> Int + | ^~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: Yes + --> 12:1..12:20 + | +12 | yes :: YesNo -> Int + | ^~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: No + --> 17:1..17:19 + | +17 | no :: YesNo -> Int + | ^~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: _ + --> 22:1..22:22 + | +22 | yesNo :: YesNo -> Int + | ^~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/266_equation_guarded/Main.purs b/tests-integration/fixtures/checking/266_equation_guarded/Main.purs new file mode 100644 index 00000000..0bfc36ba --- /dev/null +++ b/tests-integration/fixtures/checking/266_equation_guarded/Main.purs @@ -0,0 +1,9 @@ +module Main where + +testGuarded :: Boolean -> Int +testGuarded true | false = 1 +testGuarded false = 0 + +testGuardedBoth :: Boolean -> Int +testGuardedBoth true | true = 1 +testGuardedBoth false | true = 0 diff --git a/tests-integration/fixtures/checking/266_equation_guarded/Main.snap b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap new file mode 100644 index 00000000..ec73ac51 --- /dev/null +++ b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testGuarded :: Boolean -> Int +testGuardedBoth :: Boolean -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: true + --> 3:1..3:30 + | +3 | testGuarded :: Boolean -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 7:1..7:34 + | +7 | testGuardedBoth :: Boolean -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.purs b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.purs new file mode 100644 index 00000000..b8ad90ee --- /dev/null +++ b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.purs @@ -0,0 +1,29 @@ +module Main where + +data Maybe a = Just a | Nothing + +complete :: Maybe Int -> Maybe Int -> Int +complete (Just _) (Just _) = 1 +complete (Just _) Nothing = 2 +complete Nothing (Just _) = 3 +complete Nothing Nothing = 4 + +incomplete1 :: Maybe Int -> Maybe Int -> Int +incomplete1 (Just _) Nothing = 2 +incomplete1 Nothing (Just _) = 3 +incomplete1 Nothing Nothing = 4 + +incomplete2 :: Maybe Int -> Maybe Int -> Int +incomplete2 (Just _) (Just _) = 1 +incomplete2 Nothing (Just _) = 3 +incomplete2 Nothing Nothing = 4 + +incomplete3 :: Maybe Int -> Maybe Int -> Int +incomplete3 (Just _) (Just _) = 1 +incomplete3 (Just _) Nothing = 2 +incomplete3 Nothing Nothing = 4 + +incomplete4 :: Maybe Int -> Maybe Int -> Int +incomplete4 (Just _) (Just _) = 1 +incomplete4 (Just _) Nothing = 2 +incomplete4 Nothing (Just _) = 3 diff --git a/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap new file mode 100644 index 00000000..2f413548 --- /dev/null +++ b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap @@ -0,0 +1,47 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +complete :: Maybe Int -> Maybe Int -> Int +incomplete1 :: Maybe Int -> Maybe Int -> Int +incomplete2 :: Maybe Int -> Maybe Int -> Int +incomplete3 :: Maybe Int -> Maybe Int -> Int +incomplete4 :: Maybe Int -> Maybe Int -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Just _ + --> 11:1..11:45 + | +11 | incomplete1 :: Maybe Int -> Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Nothing + --> 16:1..16:45 + | +16 | incomplete2 :: Maybe Int -> Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Just _ + --> 21:1..21:45 + | +21 | incomplete3 :: Maybe Int -> Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Nothing + --> 26:1..26:45 + | +26 | incomplete4 :: Maybe Int -> Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.purs b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.purs new file mode 100644 index 00000000..c8b08014 --- /dev/null +++ b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.purs @@ -0,0 +1,17 @@ +module Main where + +data Maybe a = Just a | Nothing + +test :: Int +test = + let + f :: Maybe Int -> Int + f (Just _) = 1 + in f Nothing + +test2 :: Int +test2 = + let + g :: Maybe Int -> Int + g Nothing = 1 + in g (Just 42) diff --git a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap new file mode 100644 index 00000000..8884b51e --- /dev/null +++ b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Int +test2 :: Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing + --> 7:3..10:15 + | +7 | let + | ^~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ + --> 14:3..17:17 + | +14 | let + | ^~~ diff --git a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.purs b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.purs new file mode 100644 index 00000000..8527ab12 --- /dev/null +++ b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.purs @@ -0,0 +1,7 @@ +module Main where + +class C a where + c :: a -> Int + +instance cBool :: C Boolean where + c true = 1 diff --git a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap new file mode 100644 index 00000000..e4003caf --- /dev/null +++ b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +c :: forall (a :: Type). C (a :: Type) => (a :: Type) -> Int + +Types +C :: Type -> Constraint + +Classes +class C (&0 :: Type) + +Instances +instance C (Boolean :: Type) + chain: 0 + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: false + --> 6:1..7:13 + | +6 | instance cBool :: C Boolean where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 09b79dbc..765babfe 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -541,3 +541,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_262_exhaustive_char_partial_main() { run_test("262_exhaustive_char_partial", "Main"); } #[rustfmt::skip] #[test] fn test_263_exhaustive_string_partial_main() { run_test("263_exhaustive_string_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_264_equation_exhaustive_basic_main() { run_test("264_equation_exhaustive_basic", "Main"); } + +#[rustfmt::skip] #[test] fn test_265_equation_redundant_main() { run_test("265_equation_redundant", "Main"); } + +#[rustfmt::skip] #[test] fn test_266_equation_guarded_main() { run_test("266_equation_guarded", "Main"); } + +#[rustfmt::skip] #[test] fn test_267_equation_multiple_arguments_main() { run_test("267_equation_multiple_arguments", "Main"); } + +#[rustfmt::skip] #[test] fn test_268_let_equation_exhaustive_main() { run_test("268_let_equation_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_269_instance_equation_exhaustive_main() { run_test("269_instance_equation_exhaustive", "Main"); } From df3186a1be8066f3b04d64e2374f54e9345279ee Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Feb 2026 23:03:13 +0800 Subject: [PATCH 085/386] Extend algorithm for record fields --- .../checking/src/algorithm/binder.rs | 12 +- .../checking/src/algorithm/exhaustiveness.rs | 74 +++++--- .../src/algorithm/exhaustiveness/convert.rs | 179 +++++++++++++----- .../src/algorithm/exhaustiveness/pretty.rs | 16 ++ .../checking/061_record_binder/Main.snap | 42 ---- .../checking/224_record_shrinking/Main.snap | 7 - .../Main.snap | 5 - .../Main.snap | 5 - .../Main.purs | 24 +++ .../Main.snap | 42 ++++ tests-integration/tests/checking/generated.rs | 2 + 11 files changed, 279 insertions(+), 129 deletions(-) create mode 100644 tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index 1a3a0369..bc1bc629 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -290,7 +290,7 @@ where let record_type = state.storage.intern(Type::Application(context.prim.record, row_type)); - if let BinderMode::Check { expected_type, elaboration } = mode { + let record_type = if let BinderMode::Check { expected_type, elaboration } = mode { unification::subtype_with_mode( state, context, @@ -298,10 +298,14 @@ where expected_type, elaboration, )?; - Ok(expected_type) + expected_type } else { - Ok(record_type) - } + record_type + }; + + state.term_scope.bind_binder(binder_id, record_type); + + Ok(record_type) } lowering::BinderKind::Parenthesized { parenthesized } => { diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 463002c1..1ed108b3 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -37,6 +37,7 @@ pub enum RecordElement { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum PatternConstructor { DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Record { labels: Vec, fields: Vec }, Boolean(bool), Integer(i32), Number(bool, SmolStr), @@ -47,16 +48,26 @@ pub enum PatternConstructor { impl PatternConstructor { /// Returns the arity of this pattern constructor. /// - /// Only [`PatternConstructor::DataConstructor`] has a non-zero arity. + /// [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`] + /// have non-zero arity based on their fields. pub fn arity(&self) -> usize { - if let PatternConstructor::DataConstructor { fields, .. } = self { fields.len() } else { 0 } + match self { + PatternConstructor::DataConstructor { fields, .. } => fields.len(), + PatternConstructor::Record { fields, .. } => fields.len(), + _ => 0, + } } /// Returns the fields of this pattern constructor. /// - /// Only [`PatternConstructor::DataConstructor`] should have fields. + /// [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`] + /// have fields corresponding to their arguments. pub fn fields(&self) -> &[PatternId] { - if let PatternConstructor::DataConstructor { fields, .. } = self { fields } else { &[] } + match self { + PatternConstructor::DataConstructor { fields, .. } => fields, + PatternConstructor::Record { fields, .. } => fields, + _ => &[], + } } /// Checks if a pattern constructor matches another. @@ -69,6 +80,8 @@ impl PatternConstructor { PatternConstructor::DataConstructor { file_id: f1, item_id: i1, .. }, PatternConstructor::DataConstructor { file_id: f2, item_id: i2, .. }, ) => f1 == f2 && i1 == i2, + // Any record constructor matches any other record constructor + (PatternConstructor::Record { .. }, PatternConstructor::Record { .. }) => true, (PatternConstructor::Boolean(b1), PatternConstructor::Boolean(b2)) => b1 == b2, (PatternConstructor::Integer(i1), PatternConstructor::Integer(i2)) => i1 == i2, (PatternConstructor::Number(n1, v1), PatternConstructor::Number(n2, v2)) => { @@ -82,8 +95,9 @@ impl PatternConstructor { /// Reconstructs a [`PatternConstructor`] with the given fields. /// - /// For [`PatternConstructor::DataConstructor`], this function overrides the - /// fields. Otherwise, the fields must be empty as enforced by an assertion. + /// For [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`], + /// this function overrides the fields. Otherwise, the fields must be empty + /// as enforced by an assertion. /// /// This algorithm is used in [`algorithm_m`] to replace the fields of the /// pattern constructor we're specialising on with the fields generated by @@ -94,6 +108,10 @@ impl PatternConstructor { let fields = fields.to_vec(); PatternConstructor::DataConstructor { file_id, item_id, fields } } + PatternConstructor::Record { ref labels, .. } => { + let fields = fields.to_vec(); + PatternConstructor::Record { labels: labels.clone(), fields } + } PatternConstructor::Boolean(b) => { assert!(fields.is_empty(), "Boolean constructor has arity 0"); PatternConstructor::Boolean(b) @@ -581,15 +599,20 @@ fn specialise_vector( let first_column = &state.patterns[first_column]; if let PatternKind::Wildcard = first_column.kind { - if let PatternConstructor::DataConstructor { fields, .. } = expected { - let wildcards = fields.iter().map(|&pattern_id| { - let t = state.patterns[pattern_id].t; - state.allocate_wildcard(t) - }); - let tail_columns = tail_columns.iter().copied(); - return Some(iter::chain(wildcards, tail_columns).collect()); - } else { - return Some(tail_columns.to_vec()); + // Expand wildcard to the expected constructor's arity + match expected { + PatternConstructor::DataConstructor { fields, .. } + | PatternConstructor::Record { fields, .. } => { + let wildcards = fields.iter().map(|&pattern_id| { + let t = state.patterns[pattern_id].t; + state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } + _ => { + return Some(tail_columns.to_vec()); + } } } @@ -602,9 +625,10 @@ fn specialise_vector( return None; } - // Only DataConstructor has fields to splat + // Splat fields for constructors with arity match constructor { - PatternConstructor::DataConstructor { fields, .. } => { + PatternConstructor::DataConstructor { fields, .. } + | PatternConstructor::Record { fields, .. } => { Some(iter::chain(fields, tail_columns).copied().collect()) } _ => Some(tail_columns.to_vec()), @@ -629,6 +653,7 @@ fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { #[derive(Clone, PartialEq, Eq, Hash)] enum ConstructorKey { Data(FileId, TermItemId), + Record, Boolean(bool), Integer(i32), Number(bool, SmolStr), @@ -642,6 +667,8 @@ impl ConstructorKey { PatternConstructor::DataConstructor { file_id, item_id, .. } => { ConstructorKey::Data(*file_id, *item_id) } + // All record constructors share the same key (single constructor semantics) + PatternConstructor::Record { .. } => ConstructorKey::Record, PatternConstructor::Boolean(b) => ConstructorKey::Boolean(*b), PatternConstructor::Integer(i) => ConstructorKey::Integer(*i), PatternConstructor::Number(negative, n) => { @@ -707,6 +734,7 @@ where /// /// A sigma is complete if it contains all constructors of the data type. /// For Boolean, both true and false must be present. +/// For records, there is exactly one constructor, so any record pattern makes sigma complete. /// For other literal constructors (Integer, Number, String, Char), sigma is never complete /// (infinite domains). /// If we can't determine the type or its constructors, we conservatively return false. @@ -745,6 +773,8 @@ where Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) } + // Records have exactly one constructor, so sigma is always complete for records + PatternConstructor::Record { .. } => Ok(true), PatternConstructor::Boolean(_) => { // Boolean is complete when both true and false are present let has_true = @@ -903,7 +933,7 @@ where branches, pattern_types, |branch: &lowering::CaseBranch| (&branch.binders, &branch.guarded_expression), - ); + )?; check_exhaustiveness_core(state, context, pattern_types, unconditional) } @@ -927,7 +957,7 @@ where equations, pattern_types, |equation: &lowering::Equation| (&equation.binders, &equation.guarded), - ); + )?; check_exhaustiveness_core(state, context, pattern_types, unconditional) } @@ -938,7 +968,7 @@ fn collect_unconditional_rows( items: &[T], pattern_types: &[TypeId], to_binders: F, -) -> Vec +) -> QueryResult> where Q: ExternalQueries, F: Fn(&T) -> (&[lowering::BinderId], &Option), @@ -953,7 +983,7 @@ where let mut pattern_row = vec![]; for &binder_id in binders { - pattern_row.push(convert::convert_binder(state, context, binder_id)); + pattern_row.push(convert::convert_binder(state, context, binder_id)?); } let additional = pattern_types.iter().skip(pattern_row.len()); @@ -963,7 +993,7 @@ where pattern_rows.push(pattern_row); } } - pattern_rows + Ok(pattern_rows) } fn check_exhaustiveness_core( diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index 8d287bbe..deb1f908 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -1,15 +1,20 @@ use std::sync::Arc; +use building_types::QueryResult; use files::FileId; use indexing::TermItemId; use itertools::Itertools; use lowering::{BinderId, TermOperatorId}; +use rustc_hash::FxHashMap; use smol_str::SmolStr; use sugar::OperatorTree; +use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId, PatternKind, RecordElement}; +use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; -use crate::{ExternalQueries, TypeId}; +use crate::algorithm::toolkit; +use crate::core::{Type, TypeId}; const MISSING_NAME: SmolStr = SmolStr::new_inline(""); @@ -17,20 +22,20 @@ pub fn convert_binder( state: &mut CheckState, context: &CheckContext, id: BinderId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { let t = state.term_scope.lookup_binder(id).unwrap_or(context.prim.unknown); let Some(kind) = context.lowered.info.get_binder_kind(id) else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; match kind { lowering::BinderKind::Typed { binder, .. } => match binder { Some(id) => convert_binder(state, context, *id), - None => state.allocate_wildcard(t), + None => Ok(state.allocate_wildcard(t)), }, lowering::BinderKind::OperatorChain { .. } => { convert_operator_chain_binder(state, context, id, t) @@ -38,51 +43,51 @@ where lowering::BinderKind::Integer { value } => match value { Some(v) => { let constructor = PatternConstructor::Integer(*v); - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } - None => state.allocate_wildcard(t), + None => Ok(state.allocate_wildcard(t)), }, lowering::BinderKind::Number { negative, value } => { if let Some(value) = value { let constructor = PatternConstructor::Number(*negative, SmolStr::clone(value)); - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } else { - state.allocate_wildcard(t) + Ok(state.allocate_wildcard(t)) } } lowering::BinderKind::Constructor { resolution, arguments } => { convert_constructor_binder(state, context, resolution, arguments, t) } - lowering::BinderKind::Variable { .. } => state.allocate_wildcard(t), + lowering::BinderKind::Variable { .. } => Ok(state.allocate_wildcard(t)), lowering::BinderKind::Named { binder, .. } => match binder { Some(id) => convert_binder(state, context, *id), - None => state.allocate_wildcard(t), + None => Ok(state.allocate_wildcard(t)), }, - lowering::BinderKind::Wildcard => state.allocate_wildcard(t), + lowering::BinderKind::Wildcard => Ok(state.allocate_wildcard(t)), lowering::BinderKind::String { value, .. } => { if let Some(value) = value { let constructor = PatternConstructor::String(SmolStr::clone(value)); - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } else { - state.allocate_wildcard(t) + Ok(state.allocate_wildcard(t)) } } lowering::BinderKind::Char { value } => match value { Some(v) => { let constructor = PatternConstructor::Char(*v); - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } - None => state.allocate_wildcard(t), + None => Ok(state.allocate_wildcard(t)), }, lowering::BinderKind::Boolean { boolean } => { let constructor = PatternConstructor::Boolean(*boolean); - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { Some(id) => convert_binder(state, context, *id), - None => state.allocate_wildcard(t), + None => Ok(state.allocate_wildcard(t)), }, } } @@ -92,12 +97,15 @@ fn lower_array_binder( context: &CheckContext, array: &[BinderId], t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { - let elements = array.iter().map(|element| convert_binder(state, context, *element)).collect(); - state.allocate_pattern(PatternKind::Array { elements }, t) + let mut elements = vec![]; + for &element in array { + elements.push(convert_binder(state, context, element)?); + } + Ok(state.allocate_pattern(PatternKind::Array { elements }, t)) } fn lower_record_binder( @@ -105,20 +113,101 @@ fn lower_record_binder( context: &CheckContext, record: &[lowering::BinderRecordItem], t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { - let elements = - record.iter().map(|element| lower_record_element(state, context, element)).collect(); - state.allocate_pattern(PatternKind::Record { elements }, t) + match try_build_record_constructor(state, context, record, t)? { + Some((labels, fields)) => { + let constructor = PatternConstructor::Record { labels, fields }; + Ok(state.allocate_constructor(constructor, t)) + } + None => { + // Fallback: use the conservative path + let mut elements = vec![]; + for element in record.iter() { + elements.push(lower_record_element(state, context, element)?); + } + Ok(state.allocate_pattern(PatternKind::Record { elements }, t)) + } + } +} + +fn try_build_record_constructor( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::BinderRecordItem], + t: TypeId, +) -> QueryResult, Vec)>> +where + Q: ExternalQueries, +{ + let expanded_t = synonym::normalize_expand_type(state, context, t)?; + + let (constructor, arguments) = toolkit::extract_type_application(state, expanded_t); + + if constructor != context.prim.record { + return Ok(None); + } + + let Some(row_type_id) = arguments.first() else { + return Ok(None); + }; + + let row_type_id = state.normalize_type(*row_type_id); + let row_fields = if let Type::Row(row_type) = &state.storage[row_type_id] { + Arc::clone(&row_type.fields) + } else { + return Ok(None); + }; + + let field_type_map: FxHashMap = + row_fields.iter().map(|field| (field.label.clone(), field.id)).collect(); + + let mut provided_patterns = FxHashMap::default(); + for element in record.iter() { + match element { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name.clone() else { continue }; + let pattern = if let Some(value) = value { + convert_binder(state, context, *value)? + } else { + state.allocate_wildcard(context.prim.unknown) + }; + provided_patterns.insert(name, pattern); + } + lowering::BinderRecordItem::RecordPun { id: _, name } => { + let Some(name) = name.clone() else { continue }; + let t = field_type_map.get(&name).copied().unwrap_or(context.prim.unknown); + let pattern = state.allocate_wildcard(t); + provided_patterns.insert(name, pattern); + } + } + } + + let mut sorted_labels = field_type_map.keys().cloned().collect_vec(); + sorted_labels.sort(); + + let mut labels = Vec::with_capacity(sorted_labels.len()); + let mut fields = Vec::with_capacity(sorted_labels.len()); + + for label in sorted_labels { + let pattern = provided_patterns.get(&label).copied().unwrap_or_else(|| { + let t = field_type_map[&label]; + state.allocate_wildcard(t) + }); + labels.push(label); + fields.push(pattern); + } + + Ok(Some((labels, fields))) } fn lower_record_element( state: &mut CheckState, context: &CheckContext, element: &lowering::BinderRecordItem, -) -> RecordElement +) -> QueryResult where Q: ExternalQueries, { @@ -126,15 +215,15 @@ where lowering::BinderRecordItem::RecordField { name, value } => { let name = name.clone().unwrap_or(MISSING_NAME); let value = if let Some(value) = value { - convert_binder(state, context, *value) + convert_binder(state, context, *value)? } else { state.allocate_wildcard(context.prim.unknown) }; - RecordElement::Named(name, value) + Ok(RecordElement::Named(name, value)) } lowering::BinderRecordItem::RecordPun { name, .. } => { let name = name.clone().unwrap_or(MISSING_NAME); - RecordElement::Pun(name) + Ok(RecordElement::Pun(name)) } } } @@ -145,19 +234,21 @@ fn convert_constructor_binder( resolution: &Option<(FileId, TermItemId)>, arguments: &Arc<[BinderId]>, t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { let Some((file_id, item_id)) = *resolution else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; - let fields = - arguments.iter().map(|argument| convert_binder(state, context, *argument)).collect_vec(); + let mut fields = vec![]; + for &argument in arguments.iter() { + fields.push(convert_binder(state, context, argument)?); + } let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields }; - state.allocate_constructor(constructor, t) + Ok(state.allocate_constructor(constructor, t)) } fn convert_operator_chain_binder( @@ -165,16 +256,16 @@ fn convert_operator_chain_binder( context: &CheckContext, id: BinderId, t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { let Some(tree) = context.bracketed.binders.get(&id) else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; let Ok(tree) = tree else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; convert_operator_tree(state, context, tree, t) @@ -185,12 +276,12 @@ fn convert_operator_tree( context: &CheckContext, tree: &OperatorTree, t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { match tree { - OperatorTree::Leaf(None) => state.allocate_wildcard(t), + OperatorTree::Leaf(None) => Ok(state.allocate_wildcard(t)), OperatorTree::Leaf(Some(binder_id)) => convert_binder(state, context, *binder_id), OperatorTree::Branch(operator_id, children) => { convert_operator_branch(state, context, *operator_id, children, t) @@ -204,29 +295,29 @@ fn convert_operator_branch( operator_id: TermOperatorId, children: &[OperatorTree; 2], t: TypeId, -) -> PatternId +) -> QueryResult where Q: ExternalQueries, { let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; let Some(OperatorBranchTypes { left, right, result }) = state.term_scope.lookup_operator_node(operator_id) else { - return state.allocate_wildcard(t); + return Ok(state.allocate_wildcard(t)); }; let [left_tree, right_tree] = children; - let left_pattern = convert_operator_tree(state, context, left_tree, left); - let right_pattern = convert_operator_tree(state, context, right_tree, right); + let left_pattern = convert_operator_tree(state, context, left_tree, left)?; + let right_pattern = convert_operator_tree(state, context, right_tree, right)?; let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields: vec![left_pattern, right_pattern], }; - state.allocate_constructor(constructor, result) + Ok(state.allocate_constructor(constructor, result)) } diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index 93c0dcfe..b0bbd2ba 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -77,6 +77,22 @@ where format!("{} {}", name, field_strings.join(" ")) } + PatternConstructor::Record { labels, fields } => { + if labels.len() != fields.len() { + return "{ }".to_string(); + } + + let elements: Vec = labels + .iter() + .zip(fields.iter()) + .map(|(label, field_id)| { + let pattern_str = pretty_pattern(context, state, *field_id); + format!("{label}: {pattern_str}") + }) + .collect(); + + format!("{{ {} }}", elements.join(", ")) + } PatternConstructor::Boolean(b) => b.to_string(), PatternConstructor::Char(c) => format!("'{c}'"), PatternConstructor::String(s) => format!("\"{s}\""), diff --git a/tests-integration/fixtures/checking/061_record_binder/Main.snap b/tests-integration/fixtures/checking/061_record_binder/Main.snap index e272f781..1c4d6738 100644 --- a/tests-integration/fixtures/checking/061_record_binder/Main.snap +++ b/tests-integration/fixtures/checking/061_record_binder/Main.snap @@ -23,45 +23,3 @@ nested' :: { inner :: { x :: (t42 :: Type) | (t40 :: Row Type) } | (t41 :: Row Type) } -> (t42 :: Type) Types - -Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 3:1..3:39 - | -3 | test1 :: { x :: Int, y :: Int } -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 6:1..6:20 - | -6 | test1' { x, y } = x - | ^~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 8:1..8:64 - | -8 | test2 :: { x :: Int, y :: String } -> { x :: Int, y :: String } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 11:1..11:27 - | -11 | test2' { x, y } = { x, y } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 13:1..13:50 - | -13 | test3 :: { name :: String, age :: Int } -> String - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 16:1..16:31 - | -16 | test3' { name: n, age: a } = n - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 18:1..18:43 - | -18 | nested :: { inner :: { x :: Int } } -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 21:1..21:29 - | -21 | nested' { inner: { x } } = x - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap index dd64deb9..b79528bb 100644 --- a/tests-integration/fixtures/checking/224_record_shrinking/Main.snap +++ b/tests-integration/fixtures/checking/224_record_shrinking/Main.snap @@ -7,10 +7,3 @@ Terms test :: { a :: Int, b :: Int } -> Int Types - -Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 3:1..3:38 - | -3 | test :: { a :: Int, b :: Int } -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap index 2800977c..95be51e9 100644 --- a/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap +++ b/tests-integration/fixtures/checking/225_record_binder_additional_property/Main.snap @@ -14,8 +14,3 @@ error[AdditionalProperty]: Additional properties not allowed: b, c | 4 | test { a, b, c } = a | ^~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 3:1..3:28 - | -3 | test :: { a :: Int } -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap index 6f7ca71e..8465399f 100644 --- a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap +++ b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap @@ -14,8 +14,3 @@ error[AdditionalProperty]: Additional properties not allowed: y | 4 | test { outer: { x, y } } = x | ^~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ - --> 3:1..3:41 - | -3 | test :: { outer :: { x :: Int } } -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.purs b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.purs new file mode 100644 index 00000000..c1bc3f6f --- /dev/null +++ b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.purs @@ -0,0 +1,24 @@ +module Main where + +data Maybe a = Just a | Nothing + +-- Simple record pattern should be exhaustive +test1 :: { x :: Int, y :: Int } -> Int +test1 { x, y } = x + +-- Record with nested Maybe constructor should report missing Nothing +test2 :: { x :: Maybe Int } -> Int +test2 { x: Just n } = n + +-- Multiple record patterns - second is redundant since record is exhaustive +test3 :: { x :: Int, y :: Int } -> Int +test3 { x, y } = x +test3 r = 0 + +-- Record with multiple fields containing constructors - missing Nothing cases +test4 :: { a :: Maybe Int, b :: Maybe String } -> Int +test4 { a: Just n, b: Just _ } = n + +-- Nested record patterns should be exhaustive +test5 :: { inner :: { x :: Int } } -> Int +test5 { inner: { x } } = x diff --git a/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap new file mode 100644 index 00000000..4afed242 --- /dev/null +++ b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap @@ -0,0 +1,42 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test1 :: { x :: Int, y :: Int } -> Int +test2 :: { x :: Maybe Int } -> Int +test3 :: { x :: Int, y :: Int } -> Int +test4 :: { a :: Maybe Int, b :: Maybe String } -> Int +test5 :: { inner :: { x :: Int } } -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: { x: Nothing } + --> 10:1..10:35 + | +10 | test2 :: { x :: Maybe Int } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: _ + --> 14:1..14:39 + | +14 | test3 :: { x :: Int, y :: Int } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: { a: Nothing, b: _ } + --> 19:1..19:54 + | +19 | test4 :: { a :: Maybe Int, b :: Maybe String } -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 765babfe..562fcd21 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -553,3 +553,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_268_let_equation_exhaustive_main() { run_test("268_let_equation_exhaustive", "Main"); } #[rustfmt::skip] #[test] fn test_269_instance_equation_exhaustive_main() { run_test("269_instance_equation_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_270_record_constructor_exhaustive_main() { run_test("270_record_constructor_exhaustive", "Main"); } From 823a22c10a7de43ccb671e9d816c32b907c3823f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 02:32:47 +0800 Subject: [PATCH 086/386] Extend algorithm for array fields --- .../checking/src/algorithm/exhaustiveness.rs | 45 ++++++++++++++--- .../src/algorithm/exhaustiveness/convert.rs | 7 +-- .../src/algorithm/exhaustiveness/pretty.rs | 19 ++++---- .../checking/271_array_exhaustive/Main.purs | 18 +++++++ .../checking/271_array_exhaustive/Main.snap | 23 +++++++++ .../272_array_nested_constructor/Main.purs | 32 +++++++++++++ .../272_array_nested_constructor/Main.snap | 48 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 8 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 tests-integration/fixtures/checking/271_array_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking/271_array_exhaustive/Main.snap create mode 100644 tests-integration/fixtures/checking/272_array_nested_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 1ed108b3..024c9cfd 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -38,6 +38,7 @@ pub enum RecordElement { pub enum PatternConstructor { DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, Record { labels: Vec, fields: Vec }, + Array { fields: Vec }, Boolean(bool), Integer(i32), Number(bool, SmolStr), @@ -48,24 +49,26 @@ pub enum PatternConstructor { impl PatternConstructor { /// Returns the arity of this pattern constructor. /// - /// [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`] - /// have non-zero arity based on their fields. + /// [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`] have non-zero arity based on their fields. pub fn arity(&self) -> usize { match self { PatternConstructor::DataConstructor { fields, .. } => fields.len(), PatternConstructor::Record { fields, .. } => fields.len(), + PatternConstructor::Array { fields } => fields.len(), _ => 0, } } /// Returns the fields of this pattern constructor. /// - /// [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`] - /// have fields corresponding to their arguments. + /// [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`] have fields corresponding to their arguments. pub fn fields(&self) -> &[PatternId] { match self { PatternConstructor::DataConstructor { fields, .. } => fields, PatternConstructor::Record { fields, .. } => fields, + PatternConstructor::Array { fields } => fields, _ => &[], } } @@ -82,6 +85,11 @@ impl PatternConstructor { ) => f1 == f2 && i1 == i2, // Any record constructor matches any other record constructor (PatternConstructor::Record { .. }, PatternConstructor::Record { .. }) => true, + // Array constructors match only when their lengths match + ( + PatternConstructor::Array { fields: f1 }, + PatternConstructor::Array { fields: f2 }, + ) => f1.len() == f2.len(), (PatternConstructor::Boolean(b1), PatternConstructor::Boolean(b2)) => b1 == b2, (PatternConstructor::Integer(i1), PatternConstructor::Integer(i2)) => i1 == i2, (PatternConstructor::Number(n1, v1), PatternConstructor::Number(n2, v2)) => { @@ -95,9 +103,9 @@ impl PatternConstructor { /// Reconstructs a [`PatternConstructor`] with the given fields. /// - /// For [`PatternConstructor::DataConstructor`] and [`PatternConstructor::Record`], - /// this function overrides the fields. Otherwise, the fields must be empty - /// as enforced by an assertion. + /// For [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`], this function overrides the fields. + /// Otherwise, the fields must be empty as enforced by an assertion. /// /// This algorithm is used in [`algorithm_m`] to replace the fields of the /// pattern constructor we're specialising on with the fields generated by @@ -112,6 +120,10 @@ impl PatternConstructor { let fields = fields.to_vec(); PatternConstructor::Record { labels: labels.clone(), fields } } + PatternConstructor::Array { .. } => { + let fields = fields.to_vec(); + PatternConstructor::Array { fields } + } PatternConstructor::Boolean(b) => { assert!(fields.is_empty(), "Boolean constructor has arity 0"); PatternConstructor::Boolean(b) @@ -610,6 +622,14 @@ fn specialise_vector( let tail_columns = tail_columns.iter().copied(); return Some(iter::chain(wildcards, tail_columns).collect()); } + PatternConstructor::Array { fields } => { + let wildcards = fields.iter().map(|&pattern_id| { + let t = state.patterns[pattern_id].t; + state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } _ => { return Some(tail_columns.to_vec()); } @@ -631,6 +651,9 @@ fn specialise_vector( | PatternConstructor::Record { fields, .. } => { Some(iter::chain(fields, tail_columns).copied().collect()) } + PatternConstructor::Array { fields } => { + Some(iter::chain(fields, tail_columns).copied().collect()) + } _ => Some(tail_columns.to_vec()), } } @@ -654,6 +677,7 @@ fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { enum ConstructorKey { Data(FileId, TermItemId), Record, + Array(usize), Boolean(bool), Integer(i32), Number(bool, SmolStr), @@ -669,6 +693,8 @@ impl ConstructorKey { } // All record constructors share the same key (single constructor semantics) PatternConstructor::Record { .. } => ConstructorKey::Record, + // Array constructors are keyed by their length + PatternConstructor::Array { fields } => ConstructorKey::Array(fields.len()), PatternConstructor::Boolean(b) => ConstructorKey::Boolean(*b), PatternConstructor::Integer(i) => ConstructorKey::Integer(*i), PatternConstructor::Number(negative, n) => { @@ -775,6 +801,8 @@ where } // Records have exactly one constructor, so sigma is always complete for records PatternConstructor::Record { .. } => Ok(true), + // Arrays have infinite possible lengths, so sigma is never complete + PatternConstructor::Array { .. } => Ok(false), PatternConstructor::Boolean(_) => { // Boolean is complete when both true and false are present let has_true = @@ -841,6 +869,9 @@ where Ok(missing) } + // Arrays have infinite possible lengths, so we don't report specific missing values. + // The algorithm will fall back to wildcard suggestion. + PatternConstructor::Array { .. } => Ok(vec![]), PatternConstructor::Boolean(_) => { // Check which boolean values are missing let has_true = diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index deb1f908..153122b7 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -101,11 +101,12 @@ fn lower_array_binder( where Q: ExternalQueries, { - let mut elements = vec![]; + let mut fields = vec![]; for &element in array { - elements.push(convert_binder(state, context, element)?); + fields.push(convert_binder(state, context, element)?); } - Ok(state.allocate_pattern(PatternKind::Array { elements }, t)) + let constructor = PatternConstructor::Array { fields }; + Ok(state.allocate_constructor(constructor, t)) } fn lower_record_binder( diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index b0bbd2ba..bd28b5aa 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -82,17 +82,20 @@ where return "{ }".to_string(); } - let elements: Vec = labels - .iter() - .zip(fields.iter()) - .map(|(label, field_id)| { - let pattern_str = pretty_pattern(context, state, *field_id); - format!("{label}: {pattern_str}") - }) - .collect(); + let elements = labels.iter().zip(fields.iter()).map(|(label, field_id)| { + let pattern_str = pretty_pattern(context, state, *field_id); + format!("{label}: {pattern_str}") + }); + let elements = elements.collect_vec(); format!("{{ {} }}", elements.join(", ")) } + PatternConstructor::Array { fields } => { + let elements = fields.iter().map(|&id| pretty_pattern(context, state, id)); + + let elements = elements.collect_vec(); + format!("[{}]", elements.join(", ")) + } PatternConstructor::Boolean(b) => b.to_string(), PatternConstructor::Char(c) => format!("'{c}'"), PatternConstructor::String(s) => format!("\"{s}\""), diff --git a/tests-integration/fixtures/checking/271_array_exhaustive/Main.purs b/tests-integration/fixtures/checking/271_array_exhaustive/Main.purs new file mode 100644 index 00000000..2c236189 --- /dev/null +++ b/tests-integration/fixtures/checking/271_array_exhaustive/Main.purs @@ -0,0 +1,18 @@ +module Main where + +-- Non-exhaustive case: [] and [x] don't cover all array lengths +testNonExhaustive :: Array Int -> Int +testNonExhaustive [] = 0 +testNonExhaustive [x] = x + +-- Redundant case: two length-1 patterns, second is redundant +testRedundant :: Array Int -> Int +testRedundant [x] = x +testRedundant [y] = y +testRedundant _ = 0 + +-- Exhaustive case: wildcard covers all remaining cases +testExhaustive :: Array Int -> Int +testExhaustive [] = 0 +testExhaustive [x] = x +testExhaustive _ = 0 diff --git a/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap b/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap new file mode 100644 index 00000000..0ba5cf8e --- /dev/null +++ b/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testNonExhaustive :: Array Int -> Int +testRedundant :: Array Int -> Int +testExhaustive :: Array Int -> Int + +Types + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 4:1..4:38 + | +4 | testNonExhaustive :: Array Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: [_] + --> 9:1..9:34 + | +9 | testRedundant :: Array Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/272_array_nested_constructor/Main.purs b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.purs new file mode 100644 index 00000000..07ca081b --- /dev/null +++ b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.purs @@ -0,0 +1,32 @@ +module Main where + +data Maybe a = Just a | Nothing + +-- Array with nested Maybe constructor - missing Nothing case +testArrayWithMaybe :: Array (Maybe Int) -> Int +testArrayWithMaybe [Just n] = n + +-- Array with nested Maybe - multiple elements, missing Nothing cases +testArrayWithMultipleMaybe :: Array (Maybe Int) -> Int +testArrayWithMultipleMaybe [Just n, Just m] = n + +-- Exhaustive array with Maybe - wildcard covers remaining cases +testArrayMaybeExhaustive :: Array (Maybe Int) -> Int +testArrayMaybeExhaustive [Just n] = n +testArrayMaybeExhaustive _ = 0 + +-- Nested arrays with Maybe - complex pattern +testNestedArrayMaybe :: Array (Array (Maybe Int)) -> Int +testNestedArrayMaybe [[Just n]] = n + +-- Exhaustive inner case - all Maybe constructors covered plus wildcard for other lengths +testExhaustiveInner :: Array (Maybe Int) -> Int +testExhaustiveInner [Just n] = n +testExhaustiveInner [Nothing] = 0 +testExhaustiveInner _ = 0 + +-- Useless branch with multiple elements - second pattern is redundant +testUselessMultiple :: Array (Maybe Int) -> Int +testUselessMultiple [Just n, Just m] = n +testUselessMultiple [Just a, Just b] = a +testUselessMultiple _ = 0 diff --git a/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap new file mode 100644 index 00000000..b57ab22c --- /dev/null +++ b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap @@ -0,0 +1,48 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +testArrayWithMaybe :: Array (Maybe Int) -> Int +testArrayWithMultipleMaybe :: Array (Maybe Int) -> Int +testArrayMaybeExhaustive :: Array (Maybe Int) -> Int +testNestedArrayMaybe :: Array (Array (Maybe Int)) -> Int +testExhaustiveInner :: Array (Maybe Int) -> Int +testUselessMultiple :: Array (Maybe Int) -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 6:1..6:47 + | +6 | testArrayWithMaybe :: Array (Maybe Int) -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 10:1..10:55 + | +10 | testArrayWithMultipleMaybe :: Array (Maybe Int) -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ + --> 19:1..19:57 + | +19 | testNestedArrayMaybe :: Array (Array (Maybe Int)) -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +warning[RedundantPattern]: Pattern match has redundant branch: [Just _, Just _] + --> 29:1..29:48 + | +29 | testUselessMultiple :: Array (Maybe Int) -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 562fcd21..d18a9ad1 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -555,3 +555,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_269_instance_equation_exhaustive_main() { run_test("269_instance_equation_exhaustive", "Main"); } #[rustfmt::skip] #[test] fn test_270_record_constructor_exhaustive_main() { run_test("270_record_constructor_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_271_array_exhaustive_main() { run_test("271_array_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_272_array_nested_constructor_main() { run_test("272_array_nested_constructor", "Main"); } From a90dfd30720a228f42f817af5bf5cf86d8e8e8bc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 02:32:56 +0800 Subject: [PATCH 087/386] Update compiler scripts to show .snap.new --- compiler-scripts/src/test_runner/pending.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs index aa69dfaa..abb55867 100644 --- a/compiler-scripts/src/test_runner/pending.rs +++ b/compiler-scripts/src/test_runner/pending.rs @@ -29,6 +29,13 @@ pub struct SnapshotInfo { pub trace_path: Option, } +impl SnapshotInfo { + /// Returns the short path with `.new` suffix for display (e.g., `foo.snap.new`) + pub fn short_path_new(&self) -> String { + format!("{}.new", self.short_path) + } +} + /// Collect pending snapshots for a category, optionally filtered. pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> Vec { let pending_output = Command::new("cargo") @@ -164,14 +171,14 @@ pub fn process_pending_snapshots( ui::display_snapshot_diff( snap, &info.snap_new, - &info.short_path, + &info.short_path_new(), info.trace_path.as_deref(), args.diff, ) } else { ui::display_new_snapshot( &info.snap_new, - &info.short_path, + &info.short_path_new(), info.trace_path.as_deref(), args.diff, ) From eb97f435f57c634655932042d583144af25b089d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 03:20:37 +0800 Subject: [PATCH 088/386] Remove PatternKind::Array and PatternKind::Record --- .../checking/src/algorithm/exhaustiveness.rs | 54 ------------------- .../src/algorithm/exhaustiveness/convert.rs | 37 ++----------- .../src/algorithm/exhaustiveness/pretty.rs | 18 +------ 3 files changed, 4 insertions(+), 105 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 024c9cfd..51419c60 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -23,17 +23,9 @@ pub struct Pattern { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum PatternKind { Wildcard, - Array { elements: Vec }, - Record { elements: Vec }, Constructor { constructor: PatternConstructor }, } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum RecordElement { - Named(SmolStr, PatternId), - Pun(SmolStr), -} - #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum PatternConstructor { DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, @@ -214,9 +206,6 @@ where PatternKind::Wildcard => { algorithm_u_wildcard(state, context, matrix, vector, first_pattern.t) } - PatternKind::Array { .. } | PatternKind::Record { .. } => { - algorithm_u_other(state, context, matrix, vector) - } } } @@ -315,21 +304,6 @@ where algorithm_u(state, context, &default, &tail_columns) } -fn algorithm_u_other( - state: &mut CheckState, - context: &CheckContext, - matrix: &PatternMatrix, - vector: &PatternVector, -) -> QueryResult -where - Q: ExternalQueries, -{ - // For literals and other patterns, treat as incomplete sigma (conservative) - let default = default_matrix(state, matrix); - let tail_columns = vector[1..].to_vec(); - algorithm_u(state, context, &default, &tail_columns) -} - /// Determines the matching [`WitnessVector`] given a [`PatternMatrix`] /// and some [`PatternVector`]. /// @@ -377,9 +351,6 @@ where PatternKind::Wildcard => { algorithm_m_wildcard(state, context, matrix, vector, first_pattern.t) } - PatternKind::Array { .. } | PatternKind::Record { .. } => { - algorithm_m_other(state, context, matrix, vector, first_pattern.t) - } } } @@ -551,31 +522,6 @@ where Ok(Some(witness)) } -fn algorithm_m_other( - state: &mut CheckState, - context: &CheckContext, - matrix: &PatternMatrix, - vector: &PatternVector, - t: TypeId, -) -> QueryResult>> -where - Q: ExternalQueries, -{ - // For literals and other patterns, treat as incomplete sigma (conservative) - let default = default_matrix(state, matrix); - let tail = vector[1..].to_vec(); - - let witnesses = algorithm_m(state, context, &default, &tail)?; - - let Some(witnesses) = witnesses else { - return Ok(None); - }; - - // Prefix with wildcard - let head = state.allocate_wildcard(t); - Ok(Some(witnesses.into_iter().map(|w| iter::once(head).chain(w).collect()).collect())) -} - /// Specialises a [`PatternMatrix`] given a [`PatternConstructor`]. /// /// See documentation below for [`specialise_vector`]. diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index 153122b7..e7839925 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -10,14 +10,12 @@ use smol_str::SmolStr; use sugar::OperatorTree; use crate::ExternalQueries; -use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId, PatternKind, RecordElement}; +use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId}; use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; use crate::algorithm::toolkit; use crate::core::{Type, TypeId}; -const MISSING_NAME: SmolStr = SmolStr::new_inline(""); - pub fn convert_binder( state: &mut CheckState, context: &CheckContext, @@ -124,12 +122,8 @@ where Ok(state.allocate_constructor(constructor, t)) } None => { - // Fallback: use the conservative path - let mut elements = vec![]; - for element in record.iter() { - elements.push(lower_record_element(state, context, element)?); - } - Ok(state.allocate_pattern(PatternKind::Record { elements }, t)) + // Fallback: use a wildcard when we can't build a canonical record constructor + Ok(state.allocate_wildcard(t)) } } } @@ -204,31 +198,6 @@ where Ok(Some((labels, fields))) } -fn lower_record_element( - state: &mut CheckState, - context: &CheckContext, - element: &lowering::BinderRecordItem, -) -> QueryResult -where - Q: ExternalQueries, -{ - match element { - lowering::BinderRecordItem::RecordField { name, value } => { - let name = name.clone().unwrap_or(MISSING_NAME); - let value = if let Some(value) = value { - convert_binder(state, context, *value)? - } else { - state.allocate_wildcard(context.prim.unknown) - }; - Ok(RecordElement::Named(name, value)) - } - lowering::BinderRecordItem::RecordPun { name, .. } => { - let name = name.clone().unwrap_or(MISSING_NAME); - Ok(RecordElement::Pun(name)) - } - } -} - fn convert_constructor_binder( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index bd28b5aa..a3eafdce 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -5,9 +5,7 @@ use indexing::TermItemId; use itertools::Itertools; use crate::ExternalQueries; -use crate::algorithm::exhaustiveness::{ - PatternConstructor, PatternId, PatternKind, RecordElement, WitnessVector, -}; +use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId, PatternKind, WitnessVector}; use crate::algorithm::state::{CheckContext, CheckState}; pub fn pretty_witness( @@ -28,20 +26,6 @@ where let pattern = &state.patterns[id]; match &pattern.kind { PatternKind::Wildcard => "_".to_string(), - PatternKind::Array { elements } => { - let mut elements = elements.iter().map(|&e| pretty_pattern(context, state, e)); - format!("[{}]", elements.join(", ")) - } - PatternKind::Record { elements } => { - let mut elements = elements.iter().map(|e| match e { - RecordElement::Named(name, pattern) => { - let pattern = pretty_pattern(context, state, *pattern); - format!("{name}: {pattern}") - } - RecordElement::Pun(name) => name.to_string(), - }); - format!("{{ {} }}", elements.join(", ")) - } PatternKind::Constructor { constructor } => pretty_constructor(context, state, constructor), } } From ea644b2fea6b89571fe3a2f286a0cbd1b2213ac0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 03:46:20 +0800 Subject: [PATCH 089/386] Add dedicated error kinds for missing patterns --- .../checking/src/algorithm/exhaustiveness.rs | 4 +- .../src/algorithm/exhaustiveness/pretty.rs | 119 +++++++++++++----- compiler-core/checking/src/algorithm/state.rs | 14 +-- compiler-core/checking/src/error.rs | 7 +- compiler-core/diagnostics/src/convert.rs | 14 ++- .../checking/040_pattern_guard/Main.snap | 8 +- .../checking/060_array_binder/Main.snap | 16 +-- .../checking/255_exhaustive_basic/Main.snap | 4 +- .../256_exhaustive_multiple/Main.snap | 8 +- .../checking/257_exhaustive_tuple/Main.snap | 8 +- .../checking/258_redundant_patterns/Main.snap | 13 +- .../259_exhaustive_boolean_partial/Main.snap | 6 +- .../260_exhaustive_integer_partial/Main.snap | 4 +- .../261_exhaustive_number_partial/Main.snap | 4 +- .../262_exhaustive_char_partial/Main.snap | 4 +- .../263_exhaustive_string_partial/Main.snap | 4 +- .../264_equation_exhaustive_basic/Main.snap | 4 +- .../checking/265_equation_redundant/Main.snap | 13 +- .../checking/266_equation_guarded/Main.snap | 4 +- .../267_equation_multiple_arguments/Main.snap | 8 +- .../268_let_equation_exhaustive/Main.snap | 4 +- .../Main.snap | 2 +- .../Main.snap | 6 +- .../checking/271_array_exhaustive/Main.snap | 4 +- .../272_array_nested_constructor/Main.snap | 8 +- 25 files changed, 171 insertions(+), 119 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 51419c60..cbb409d3 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -887,8 +887,8 @@ where } pub struct ExhaustivenessReport { - pub missing: Option>, - pub redundant: Vec, + pub missing: Option>, + pub redundant: Vec, } pub fn check_case_patterns( diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index a3eafdce..e9e17946 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use files::FileId; use indexing::TermItemId; -use itertools::Itertools; +use smol_str::{SmolStr, SmolStrBuilder}; use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId, PatternKind, WitnessVector}; @@ -12,84 +12,137 @@ pub fn pretty_witness( context: &CheckContext, state: &CheckState, witness: &WitnessVector, -) -> String +) -> SmolStr where Q: ExternalQueries, { - witness.iter().map(|&id| pretty_pattern(context, state, id)).join(", ") + join_smolstr(witness.iter().map(|&id| pretty_pattern(context, state, id)), ", ") } -fn pretty_pattern(context: &CheckContext, state: &CheckState, id: PatternId) -> String +fn pretty_pattern(context: &CheckContext, state: &CheckState, id: PatternId) -> SmolStr where Q: ExternalQueries, { let pattern = &state.patterns[id]; match &pattern.kind { - PatternKind::Wildcard => "_".to_string(), + PatternKind::Wildcard => SmolStr::new_inline("_"), PatternKind::Constructor { constructor } => pretty_constructor(context, state, constructor), } } +fn join_smolstr(iterator: impl Iterator, separator: &str) -> SmolStr { + let mut builder = SmolStrBuilder::default(); + join_with_sep(&mut builder, iterator, separator, |builder, item| builder.push_str(&item)); + builder.finish() +} + +fn join_with_sep( + builder: &mut SmolStrBuilder, + iter: impl Iterator, + sep: &str, + mut render: impl FnMut(&mut SmolStrBuilder, T), +) { + let mut first = true; + for item in iter { + if !first { + builder.push_str(sep); + } + first = false; + render(builder, item); + } +} + fn pretty_constructor( context: &CheckContext, state: &CheckState, constructor: &PatternConstructor, -) -> String +) -> SmolStr where Q: ExternalQueries, { match constructor { PatternConstructor::DataConstructor { file_id, item_id, fields } => { let name = lookup_constructor_name(context, *file_id, *item_id) - .unwrap_or_else(|| "".to_string()); + .unwrap_or_else(|| SmolStr::new_inline("")); if fields.is_empty() { return name; } - let mut field_strings = fields.iter().map(|&id| { + let mut builder = SmolStrBuilder::default(); + builder.push_str(&name); + + for &id in fields.iter() { + builder.push(' '); let rendered = pretty_pattern(context, state, id); let pattern = &state.patterns[id]; if let PatternKind::Constructor { constructor } = &pattern.kind && !constructor.fields().is_empty() { - format!("({rendered})") + builder.push('('); + builder.push_str(&rendered); + builder.push(')'); } else { - rendered + builder.push_str(&rendered); } - }); + } - format!("{} {}", name, field_strings.join(" ")) + builder.finish() } PatternConstructor::Record { labels, fields } => { if labels.len() != fields.len() { - return "{ }".to_string(); + return SmolStr::new_inline("{ }"); } - let elements = labels.iter().zip(fields.iter()).map(|(label, field_id)| { - let pattern_str = pretty_pattern(context, state, *field_id); - format!("{label}: {pattern_str}") - }); - - let elements = elements.collect_vec(); - format!("{{ {} }}", elements.join(", ")) + let mut builder = SmolStrBuilder::default(); + builder.push_str("{ "); + join_with_sep( + &mut builder, + labels.iter().zip(fields.iter()), + ", ", + |b, (label, field_id)| { + let field = pretty_pattern(context, state, *field_id); + b.push_str(label); + b.push_str(": "); + b.push_str(&field); + }, + ); + builder.push_str(" }"); + builder.finish() } PatternConstructor::Array { fields } => { - let elements = fields.iter().map(|&id| pretty_pattern(context, state, id)); - - let elements = elements.collect_vec(); - format!("[{}]", elements.join(", ")) + let mut builder = SmolStrBuilder::default(); + builder.push('['); + join_with_sep(&mut builder, fields.iter().copied(), ", ", |b, id| { + let rendered = pretty_pattern(context, state, id); + b.push_str(&rendered); + }); + builder.push(']'); + builder.finish() + } + PatternConstructor::Boolean(b) => SmolStr::from(b.to_string()), + PatternConstructor::Char(c) => { + let mut builder = SmolStrBuilder::default(); + builder.push('\''); + builder.push(*c); + builder.push('\''); + builder.finish() + } + PatternConstructor::String(s) => { + let mut builder = SmolStrBuilder::default(); + builder.push('"'); + builder.push_str(s); + builder.push('"'); + builder.finish() } - PatternConstructor::Boolean(b) => b.to_string(), - PatternConstructor::Char(c) => format!("'{c}'"), - PatternConstructor::String(s) => format!("\"{s}\""), - PatternConstructor::Integer(i) => i.to_string(), + PatternConstructor::Integer(i) => SmolStr::from(i.to_string()), PatternConstructor::Number(negative, n) => { + let mut builder = SmolStrBuilder::default(); if *negative { - format!("-{n}") - } else { - n.to_string() + builder.push('-'); } + builder.push_str(&n.to_string()); + builder.finish() } } } @@ -98,7 +151,7 @@ fn lookup_constructor_name( context: &CheckContext, file_id: FileId, term_id: TermItemId, -) -> Option +) -> Option where Q: ExternalQueries, { @@ -109,5 +162,5 @@ where }; let item = &indexed.items[term_id]; - item.name.as_ref().map(|name| name.to_string()) + item.name.as_ref().map(SmolStr::clone) } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 5bd10ac0..4c141732 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1007,16 +1007,14 @@ impl CheckState { } pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { - if let Some(missing) = exhaustiveness.missing { - let message = - format!("Pattern match is not exhaustive. Missing: {}", missing.join(", ")); - let message_id = self.intern_error_message(message); - self.insert_error(ErrorKind::CustomWarning { message_id }); + if let Some(patterns) = exhaustiveness.missing { + let patterns = Arc::from(patterns); + self.insert_error(ErrorKind::MissingPatterns { patterns }); } - for redundant in exhaustiveness.redundant { - let pattern = self.intern_error_message(redundant); - self.insert_error(ErrorKind::RedundantPattern { pattern }); + if !exhaustiveness.redundant.is_empty() { + let patterns = Arc::from(exhaustiveness.redundant); + self.insert_error(ErrorKind::RedundantPatterns { patterns }); } } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index 5daebc6b..dc3b5954 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -124,8 +124,11 @@ pub enum ErrorKind { CustomWarning { message_id: TypeErrorMessageId, }, - RedundantPattern { - pattern: TypeErrorMessageId, + RedundantPatterns { + patterns: Arc<[SmolStr]>, + }, + MissingPatterns { + patterns: Arc<[SmolStr]>, }, CustomFailure { message_id: TypeErrorMessageId, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index c2de7538..0b8a8af0 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -313,12 +313,20 @@ impl ToDiagnostics for CheckError { "CoercibleConstructorNotInScope", "Constructor not in scope for Coercible".to_string(), ), - ErrorKind::RedundantPattern { pattern } => { - let pattern = lookup_message(*pattern); + ErrorKind::RedundantPatterns { patterns } => { + let patterns = patterns.join(", "); ( Severity::Warning, "RedundantPattern", - format!("Pattern match has redundant branch: {pattern}"), + format!("Pattern match has redundant patterns: {patterns}"), + ) + } + ErrorKind::MissingPatterns { patterns } => { + let patterns = patterns.join(", "); + ( + Severity::Warning, + "MissingPatterns", + format!("Pattern match is not exhaustive. Missing: {patterns}"), ) } ErrorKind::CustomWarning { message_id } => { diff --git a/tests-integration/fixtures/checking/040_pattern_guard/Main.snap b/tests-integration/fixtures/checking/040_pattern_guard/Main.snap index 74103fd6..d6894e5e 100644 --- a/tests-integration/fixtures/checking/040_pattern_guard/Main.snap +++ b/tests-integration/fixtures/checking/040_pattern_guard/Main.snap @@ -12,22 +12,22 @@ bar' :: forall (t4 :: Type). (t4 :: Type) -> String Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 3:1..3:18 | 3 | foo :: Int -> Int | ^~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 6:1..6:24 | 6 | bar :: String -> String | ^~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 9:1..9:21 | 9 | foo' x | c <- 42 = c | ^~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 11:1..11:26 | 11 | bar' s | b <- "hello" = b diff --git a/tests-integration/fixtures/checking/060_array_binder/Main.snap b/tests-integration/fixtures/checking/060_array_binder/Main.snap index fc3ea2ff..62e3b29b 100644 --- a/tests-integration/fixtures/checking/060_array_binder/Main.snap +++ b/tests-integration/fixtures/checking/060_array_binder/Main.snap @@ -16,42 +16,42 @@ nested' :: forall (t53 :: Type). Array (Array (t53 :: Type)) -> (t53 :: Type) Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 3:1..3:45 | 3 | test1 :: Array Int -> { x :: Int, y :: Int } | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 6:1..6:25 | 6 | test1' [x, y] = { x, y } | ^~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:1..8:38 | 8 | test2 :: forall a. Array a -> Array a | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 11:1..11:29 | 11 | test2' [x, y, z] = [z, y, x] | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 13:1..13:26 | 13 | test3 :: Array Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 18:1..18:14 | 18 | test3' [] = 0 | ^~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 22:1..22:35 | 22 | nested :: Array (Array Int) -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 26:1..26:18 | 26 | nested' [[x]] = x diff --git a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap index 5a0be2c8..6950447c 100644 --- a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap +++ b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap @@ -22,12 +22,12 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing --> 5:9..6:14 | 5 | test1 = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _ --> 8:9..9:15 | 8 | test2 = case _ of diff --git a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap index ce56198b..d781101d 100644 --- a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap +++ b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap @@ -25,22 +25,22 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _, Just _ --> 11:15..14:24 | 11 | incomplete1 = case _, _ of | ^~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _, Nothing --> 16:15..19:24 | 16 | incomplete2 = case _, _ of | ^~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing, Just _ --> 21:15..24:24 | 21 | incomplete3 = case _, _ of | ^~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing, Nothing --> 26:15..29:23 | 26 | incomplete4 = case _, _ of diff --git a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap index 03b37868..8e78ae6a 100644 --- a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap +++ b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap @@ -37,22 +37,22 @@ Tuple = [Representational, Representational] Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple (Just _) (Just _) +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Tuple (Just _) (Just _) --> 13:15..16:29 | 13 | incomplete1 = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple (Just _) Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Tuple (Just _) Nothing --> 18:15..21:29 | 18 | incomplete2 = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple Nothing (Just _) +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Tuple Nothing (Just _) --> 23:15..26:29 | 23 | incomplete3 = case _ of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Tuple Nothing Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Tuple Nothing Nothing --> 28:15..31:30 | 28 | incomplete4 = case _ of diff --git a/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap index daae64e1..9514b893 100644 --- a/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap +++ b/tests-integration/fixtures/checking/258_redundant_patterns/Main.snap @@ -31,27 +31,22 @@ Unit = [] YesNo = [] Diagnostics -warning[RedundantPattern]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant patterns: _, Unit --> 5:8..8:12 | 5 | unit = case _ of | ^~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: Unit - --> 5:8..8:12 - | -5 | unit = case _ of - | ^~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: Yes +warning[RedundantPattern]: Pattern match has redundant patterns: Yes --> 12:7..15:11 | 12 | yes = case _ of | ^~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: No +warning[RedundantPattern]: Pattern match has redundant patterns: No --> 17:6..20:10 | 17 | no = case _ of | ^~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant patterns: _ --> 22:9..25:9 | 22 | yesNo = case _ of diff --git a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap index 1cf8b5d5..d147a022 100644 --- a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap +++ b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap @@ -13,17 +13,17 @@ testWildcardBoolean :: Boolean -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: false +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: false --> 4:21..5:12 | 4 | testPartialTrue b = case b of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: true +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: true --> 8:22..9:13 | 8 | testPartialFalse b = case b of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: false +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: false --> 18:11..19:14 | 18 | true -> case y of diff --git a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap index 15753010..65c7f66a 100644 --- a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap +++ b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap @@ -11,12 +11,12 @@ testWildcard :: Int -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 4:21..5:9 | 4 | testPartialZero n = case n of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:20..9:9 | 8 | testPartialOne n = case n of diff --git a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap index 20041b4d..81121e46 100644 --- a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap +++ b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap @@ -11,12 +11,12 @@ testWildcard :: Number -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 4:21..5:11 | 4 | testPartialZero n = case n of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:24..9:11 | 8 | testPartialOneFive n = case n of diff --git a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap index 964f4470..27767cbb 100644 --- a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap +++ b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap @@ -11,12 +11,12 @@ testWildcard :: Char -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 4:18..5:11 | 4 | testPartialA c = case c of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:18..9:11 | 8 | testPartialB c = case c of diff --git a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap index 95f53d87..d36983fa 100644 --- a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap +++ b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap @@ -11,12 +11,12 @@ testWildcard :: String -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 4:22..5:15 | 4 | testPartialHello s = case s of | ^~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:22..9:15 | 8 | testPartialWorld s = case s of diff --git a/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap index ae9f8bbf..95159119 100644 --- a/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap +++ b/tests-integration/fixtures/checking/264_equation_exhaustive_basic/Main.snap @@ -22,12 +22,12 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing --> 5:1..5:26 | 5 | test1 :: Maybe Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _ --> 8:1..8:26 | 8 | test2 :: Maybe Int -> Int diff --git a/tests-integration/fixtures/checking/265_equation_redundant/Main.snap b/tests-integration/fixtures/checking/265_equation_redundant/Main.snap index cb99395a..02b574a8 100644 --- a/tests-integration/fixtures/checking/265_equation_redundant/Main.snap +++ b/tests-integration/fixtures/checking/265_equation_redundant/Main.snap @@ -31,27 +31,22 @@ Unit = [] YesNo = [] Diagnostics -warning[RedundantPattern]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant patterns: _, Unit --> 5:1..5:20 | 5 | unit :: Unit -> Int | ^~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: Unit - --> 5:1..5:20 - | -5 | unit :: Unit -> Int - | ^~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: Yes +warning[RedundantPattern]: Pattern match has redundant patterns: Yes --> 12:1..12:20 | 12 | yes :: YesNo -> Int | ^~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: No +warning[RedundantPattern]: Pattern match has redundant patterns: No --> 17:1..17:19 | 17 | no :: YesNo -> Int | ^~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant patterns: _ --> 22:1..22:22 | 22 | yesNo :: YesNo -> Int diff --git a/tests-integration/fixtures/checking/266_equation_guarded/Main.snap b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap index ec73ac51..56ecb058 100644 --- a/tests-integration/fixtures/checking/266_equation_guarded/Main.snap +++ b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap @@ -10,12 +10,12 @@ testGuardedBoth :: Boolean -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: true +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: true --> 3:1..3:30 | 3 | testGuarded :: Boolean -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 7:1..7:34 | 7 | testGuardedBoth :: Boolean -> Int diff --git a/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap index 2f413548..7f029315 100644 --- a/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap +++ b/tests-integration/fixtures/checking/267_equation_multiple_arguments/Main.snap @@ -25,22 +25,22 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _, Just _ --> 11:1..11:45 | 11 | incomplete1 :: Maybe Int -> Maybe Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _, Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _, Nothing --> 16:1..16:45 | 16 | incomplete2 :: Maybe Int -> Maybe Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing, Just _ --> 21:1..21:45 | 21 | incomplete3 :: Maybe Int -> Maybe Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing, Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing, Nothing --> 26:1..26:45 | 26 | incomplete4 :: Maybe Int -> Maybe Int -> Int diff --git a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap index 8884b51e..7579c10e 100644 --- a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap @@ -22,12 +22,12 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Nothing +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing --> 7:3..10:15 | 7 | let | ^~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: Just _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _ --> 14:3..17:17 | 14 | let diff --git a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap index e4003caf..b631fa3b 100644 --- a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap @@ -17,7 +17,7 @@ instance C (Boolean :: Type) chain: 0 Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: false +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: false --> 6:1..7:13 | 6 | instance cBool :: C Boolean where diff --git a/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap index 4afed242..696d8f24 100644 --- a/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/270_record_constructor_exhaustive/Main.snap @@ -25,17 +25,17 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: { x: Nothing } +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: { x: Nothing } --> 10:1..10:35 | 10 | test2 :: { x :: Maybe Int } -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: _ +warning[RedundantPattern]: Pattern match has redundant patterns: _ --> 14:1..14:39 | 14 | test3 :: { x :: Int, y :: Int } -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: { a: Nothing, b: _ } +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: { a: Nothing, b: _ } --> 19:1..19:54 | 19 | test4 :: { a :: Maybe Int, b :: Maybe String } -> Int diff --git a/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap b/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap index 0ba5cf8e..088d9b6d 100644 --- a/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/271_array_exhaustive/Main.snap @@ -11,12 +11,12 @@ testExhaustive :: Array Int -> Int Types Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 4:1..4:38 | 4 | testNonExhaustive :: Array Int -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: [_] +warning[RedundantPattern]: Pattern match has redundant patterns: [_] --> 9:1..9:34 | 9 | testRedundant :: Array Int -> Int diff --git a/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap index b57ab22c..d3dcf2a2 100644 --- a/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap +++ b/tests-integration/fixtures/checking/272_array_nested_constructor/Main.snap @@ -26,22 +26,22 @@ Roles Maybe = [Representational] Diagnostics -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 6:1..6:47 | 6 | testArrayWithMaybe :: Array (Maybe Int) -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 10:1..10:55 | 10 | testArrayWithMultipleMaybe :: Array (Maybe Int) -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[CustomWarning]: Pattern match is not exhaustive. Missing: _ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 19:1..19:57 | 19 | testNestedArrayMaybe :: Array (Array (Maybe Int)) -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[RedundantPattern]: Pattern match has redundant branch: [Just _, Just _] +warning[RedundantPattern]: Pattern match has redundant patterns: [Just _, Just _] --> 29:1..29:48 | 29 | testUselessMultiple :: Array (Maybe Int) -> Int From d778d40ec73dfa098fb61d837a13ef19d3887552 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 14:13:29 +0800 Subject: [PATCH 090/386] Fix missing instantiations in argument positions This adds instantiation in check_expression_argument if the expected type is monomorphic; and instantiation in infer_lambda for the body --- compiler-core/checking/src/algorithm/term.rs | 14 +++++- .../checking/src/algorithm/toolkit.rs | 6 +++ .../273_class_member_instantiation/Main.purs | 24 ++++++++++ .../273_class_member_instantiation/Main.snap | 47 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/273_class_member_instantiation/Main.purs create mode 100644 tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index c12cbb76..28ecb89b 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -314,6 +314,17 @@ where { state.with_error_step(ErrorStep::CheckingExpression(expr_id), |state| { let inferred = infer_expression_quiet(state, context, expr_id)?; + // Instantiate the inferred type when the expected type is not + // polymorphic, so constraints are collected as wanted rather + // than leaking into unification. Skipped for higher-rank + // arguments where constraints must match structurally. + let expected = state.normalize_type(expected); + let inferred = + if matches!(state.storage[expected], Type::Forall(..) | Type::Constrained(..)) { + inferred + } else { + toolkit::instantiate_constrained(state, inferred) + }; unification::subtype_with_mode(state, context, inferred, expected, ElaborationMode::No)?; crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) @@ -592,7 +603,8 @@ where } let result_type = if let Some(body) = expression { - infer_expression(state, context, body)? + let body_type = infer_expression(state, context, body)?; + toolkit::instantiate_constrained(state, body_type) } else { state.fresh_unification_type(context) }; diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index d6d9b8a0..297de20d 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -122,6 +122,12 @@ pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeI } } +/// [`instantiate_forall`] then [`collect_constraints`]. +pub fn instantiate_constrained(state: &mut CheckState, type_id: TypeId) -> TypeId { + let type_id = instantiate_forall(state, type_id); + collect_constraints(state, type_id) +} + /// Instantiates [`Type::Forall`] with the provided arguments. /// /// This function falls back to constructing skolem variables if there's diff --git a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.purs b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.purs new file mode 100644 index 00000000..8a907bb7 --- /dev/null +++ b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.purs @@ -0,0 +1,24 @@ +module Main where + +data Unit = Unit + +class Monoid a where + mempty :: a + +class Semigroup a where + append :: a -> a -> a + +foreign import apply :: forall a b. (a -> b) -> a -> b +foreign import pure :: forall m a. a -> m a +foreign import lift2 :: forall m a b c. (a -> b -> c) -> m a -> m b -> m c + +test1 :: forall a. Monoid a => Unit -> a +test1 _ = mempty + +test2 = \_ -> mempty + +test3 = apply (\x -> x) mempty + +test4 = pure mempty + +test5 = lift2 append diff --git a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap new file mode 100644 index 00000000..d61b7443 --- /dev/null +++ b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap @@ -0,0 +1,47 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +mempty :: forall (a :: Type). Monoid (a :: Type) => (a :: Type) +append :: forall (a :: Type). Semigroup (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +apply :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) +pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) +lift2 :: + forall (m :: Type -> Type) (a :: Type) (b :: Type) (c :: Type). + ((a :: Type) -> (b :: Type) -> (c :: Type)) -> + (m :: Type -> Type) (a :: Type) -> + (m :: Type -> Type) (b :: Type) -> + (m :: Type -> Type) (c :: Type) +test1 :: forall (a :: Type). Monoid (a :: Type) => Unit -> (a :: Type) +test2 :: forall (t22 :: Type) (t23 :: Type). Monoid (t23 :: Type) => (t22 :: Type) -> (t23 :: Type) +test3 :: forall (t24 :: Type). Monoid (t24 :: Type) => (t24 :: Type) +test4 :: + forall (t32 :: Type -> Type) (t33 :: Type). + Monoid (t33 :: Type) => (t32 :: Type -> Type) (t33 :: Type) +test5 :: + forall (t37 :: Type -> Type) (t40 :: Type). + Semigroup (t40 :: Type) => + (t37 :: Type -> Type) (t40 :: Type) -> + (t37 :: Type -> Type) (t40 :: Type) -> + (t37 :: Type -> Type) (t40 :: Type) + +Types +Unit :: Type +Monoid :: Type -> Constraint +Semigroup :: Type -> Constraint + +Data +Unit + Quantified = :0 + Kind = :0 + + +Roles +Unit = [] + +Classes +class Monoid (&0 :: Type) +class Semigroup (&0 :: Type) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index d18a9ad1..3492baad 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -559,3 +559,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_271_array_exhaustive_main() { run_test("271_array_exhaustive", "Main"); } #[rustfmt::skip] #[test] fn test_272_array_nested_constructor_main() { run_test("272_array_nested_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_273_class_member_instantiation_main() { run_test("273_class_member_instantiation", "Main"); } From ca9377a29fa120f7ad9abc7d459ee91af7510197 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 16:16:33 +0800 Subject: [PATCH 091/386] Fix scoping issues for given constraints and let bindings This commit also adds a dedicated ErrorStep for let binding groups such that errors are reported more accurately for inspection in snapshots and the IDE. --- Cargo.lock | 1 + compiler-bin/src/lsp/event.rs | 3 +- compiler-compatibility/src/compat.rs | 15 +++- compiler-core/checking/Cargo.toml | 1 + .../checking/src/algorithm/constraint.rs | 6 +- compiler-core/checking/src/algorithm/state.rs | 25 +++++- compiler-core/checking/src/algorithm/term.rs | 89 +++++++++++++------ compiler-core/checking/src/error.rs | 2 + compiler-core/checking/src/trace.rs | 42 ++++++++- compiler-core/diagnostics/src/context.rs | 38 +++++++- .../checking/041_where_expression/Main.snap | 5 +- .../checking/042_where_polymorphic/Main.snap | 3 +- .../080_let_recursive_errors/Main.snap | 16 ++-- .../221_do_let_annotation_solve/Main.snap | 6 +- .../223_ado_let_annotation_solve/Main.snap | 6 +- .../268_let_equation_exhaustive/Main.snap | 12 +-- .../checking/274_givens_retained/Main.purs | 10 +++ .../checking/274_givens_retained/Main.snap | 14 +++ .../checking/275_givens_scoped/Main.purs | 15 ++++ .../checking/275_givens_scoped/Main.snap | 21 +++++ tests-integration/src/generated/basic.rs | 3 +- tests-integration/tests/checking/generated.rs | 4 + 22 files changed, 274 insertions(+), 63 deletions(-) create mode 100644 tests-integration/fixtures/checking/274_givens_retained/Main.purs create mode 100644 tests-integration/fixtures/checking/274_givens_retained/Main.snap create mode 100644 tests-integration/fixtures/checking/275_givens_scoped/Main.purs create mode 100644 tests-integration/fixtures/checking/275_givens_scoped/Main.snap diff --git a/Cargo.lock b/Cargo.lock index f86a3a70..c30e0b9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "smol_str", "stabilizing", "sugar", + "syntax", "tracing", ] diff --git a/compiler-bin/src/lsp/event.rs b/compiler-bin/src/lsp/event.rs index eea66bb9..34ab44a0 100644 --- a/compiler-bin/src/lsp/event.rs +++ b/compiler-bin/src/lsp/event.rs @@ -50,7 +50,8 @@ fn collect_diagnostics_core( common::file_uri(&snapshot.engine, &files, id)? }; - let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &checked); + let context = + DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &lowered, &checked); let mut all_diagnostics = vec![]; diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/src/compat.rs index 5f82f81d..8f65b3ef 100644 --- a/compiler-compatibility/src/compat.rs +++ b/compiler-compatibility/src/compat.rs @@ -140,6 +140,18 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & let resolved = engine.resolved(id); let checked = engine.checked(id); + let lowered_ref = match &lowered { + Ok(l) => l, + Err(_) => { + return FileResult { + relative_path: relative_path.to_string(), + error_count: 1, + warning_count: 0, + output: format!("{relative_path}:1:1: error[LowerError]: Failed to lower\n"), + }; + } + }; + let checked_ref = match &checked { Ok(c) => c, Err(_) => { @@ -152,7 +164,8 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & } }; - let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, checked_ref); + let context = + DiagnosticsContext::new(&content, &root, &stabilized, &indexed, lowered_ref, checked_ref); let mut all_diagnostics = vec![]; diff --git a/compiler-core/checking/Cargo.toml b/compiler-core/checking/Cargo.toml index e38cfe9e..e3cf5c0d 100644 --- a/compiler-core/checking/Cargo.toml +++ b/compiler-core/checking/Cargo.toml @@ -24,3 +24,4 @@ rustc-hash = "2.1.1" smol_str = "0.3.4" stabilizing = { version = "0.1.0", path = "../stabilizing" } sugar = { version = "0.1.0", path = "../sugar" } +syntax = { version = "0.1.0", path = "../syntax" } diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index f936a2a8..9acb218a 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -31,7 +31,7 @@ pub fn solve_constraints( state: &mut CheckState, context: &CheckContext, wanted: VecDeque, - given: Vec, + given: &[TypeId], ) -> QueryResult> where Q: ExternalQueries, @@ -144,14 +144,14 @@ pub(crate) fn constraint_application( fn elaborate_given( state: &mut CheckState, context: &CheckContext, - given: Vec, + given: &[TypeId], ) -> QueryResult> where Q: ExternalQueries, { let mut elaborated = vec![]; - for constraint in given { + for &constraint in given { elaborated.push(constraint); elaborate_superclasses(state, context, constraint, &mut elaborated)?; } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 4c141732..1ab3c056 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1003,7 +1003,30 @@ impl CheckState { Q: ExternalQueries, { let (wanted, given) = self.constraints.take(); - constraint::solve_constraints(self, context, wanted, given) + constraint::solve_constraints(self, context, wanted, &given) + } + + pub fn with_local_givens(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { + let length = self.constraints.given.len(); + let result = action(self); + self.constraints.given.drain(length..); + result + } + + pub fn solve_constraints_local( + &mut self, + context: &CheckContext, + ) -> QueryResult> + where + Q: ExternalQueries, + { + let (wanted, given) = self.constraints.take(); + let residuals = constraint::solve_constraints(self, context, wanted, &given); + + let after_solve = mem::replace(&mut self.constraints.given, given); + debug_assert!(after_solve.is_empty(), "invariant violated: non-empty givens"); + + residuals } pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 28ecb89b..8bc1b224 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -116,6 +116,35 @@ pub fn check_equations( signature: inspect::InspectSignature, equations: &[lowering::Equation], ) -> QueryResult<()> +where + Q: ExternalQueries, +{ + check_equations_core(state, context, signature_id, &signature, equations)?; + + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; + state.report_exhaustiveness(exhaustiveness); + + let residual = state.solve_constraints(context)?; + for constraint in residual { + let constraint = state.render_local_type(context, constraint); + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + + if let Some(variable) = signature.variables.first() { + state.type_scope.unbind(variable.level); + } + + Ok(()) +} + +fn check_equations_core( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + signature: &inspect::InspectSignature, + equations: &[lowering::Equation], +) -> QueryResult<()> where Q: ExternalQueries, { @@ -196,20 +225,6 @@ where } } - let exhaustiveness = - exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; - state.report_exhaustiveness(exhaustiveness); - - let residual = state.solve_constraints(context)?; - for constraint in residual { - let constraint = state.render_local_type(context, constraint); - state.insert_error(ErrorKind::NoInstanceFound { constraint }); - } - - if let Some(variable) = signature.variables.first() { - state.type_scope.unbind(variable.level); - } - Ok(()) } @@ -1940,6 +1955,21 @@ fn check_let_name_binding( context: &CheckContext, id: lowering::LetBindingNameGroupId, ) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_local_givens(|state| { + state.with_error_step(ErrorStep::CheckingLetName(id), |state| { + check_let_name_binding_core(state, context, id) + }) + }) +} + +fn check_let_name_binding_core( + state: &mut CheckState, + context: &CheckContext, + id: lowering::LetBindingNameGroupId, +) -> QueryResult<()> where Q: ExternalQueries, { @@ -1951,31 +1981,32 @@ where return Ok(()); }; - if let Some(signature_id) = name.signature { + let exhaustiveness = if let Some(signature_id) = name.signature { let surface_bindings = state.surface_bindings.get_let(id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); let signature = inspect::inspect_signature_core(state, context, name_type, surface_bindings)?; - check_equations(state, context, signature_id, signature, &name.equations) + check_equations_core(state, context, signature_id, &signature, &name.equations)?; + + let pattern_types = &signature.arguments; + exhaustiveness::check_equation_patterns(state, context, pattern_types, &name.equations)? } else { infer_equations_core(state, context, name_type, &name.equations)?; let (pattern_types, _) = toolkit::extract_function_arguments(state, name_type); - let exhaustiveness = exhaustiveness::check_equation_patterns( - state, - context, - &pattern_types, - &name.equations, - )?; - state.report_exhaustiveness(exhaustiveness); + exhaustiveness::check_equation_patterns(state, context, &pattern_types, &name.equations)? + }; - // No let-generalization: infer equations and solve constraints; - // residuals are deferred to parent scope for later error reporting. - let residual = state.solve_constraints(context)?; - state.constraints.extend_wanted(&residual); + state.report_exhaustiveness(exhaustiveness); - Ok(()) - } + // PureScript does not have let generalisation; residuals are moved + // to the parent scope's wanted constraints. Given constraints must + // also be preserved across let bindings. This is demonstrated by + // 274_givens_retained, 275_givens_scoped + let residual = state.solve_constraints_local(context)?; + state.constraints.extend_wanted(&residual); + + Ok(()) } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index dc3b5954..a8e9282a 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -30,6 +30,8 @@ pub enum ErrorStep { InferringAdoMap(lowering::DoStatementId), InferringAdoApply(lowering::DoStatementId), CheckingAdoLet(lowering::DoStatementId), + + CheckingLetName(lowering::LetBindingNameGroupId), } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/compiler-core/checking/src/trace.rs b/compiler-core/checking/src/trace.rs index 551ff4e4..47e5b949 100644 --- a/compiler-core/checking/src/trace.rs +++ b/compiler-core/checking/src/trace.rs @@ -5,13 +5,33 @@ //! `no-tracing` feature is enabled. use building_types::QueryResult; +use syntax::SyntaxNodePtr; use crate::ExternalQueries; use crate::algorithm::state::CheckContext; use crate::error::ErrorStep; -/// Extracts the byte offset range from an error step. -pub fn step_byte_range(step: &ErrorStep, context: &CheckContext) -> Option<(u32, u32)> +fn spanning_byte_range(iterator: I) -> Option<(u32, u32)> +where + I: IntoIterator, + I::IntoIter: DoubleEndedIterator, +{ + let mut iter = iterator.into_iter(); + + let start = iter.next()?; + let end = iter.next_back().unwrap_or(start); + + let start = start.text_range(); + let end = end.text_range(); + + let range = start.cover(end); + let start = range.start().into(); + let end = range.end().into(); + + Some((start, end)) +} + +fn step_byte_range(step: &ErrorStep, context: &CheckContext) -> Option<(u32, u32)> where Q: ExternalQueries, { @@ -38,6 +58,23 @@ where ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) | ErrorStep::CheckingAdoLet(id) => context.stabilized.syntax_ptr(*id)?, + ErrorStep::CheckingLetName(id) => { + let group = context.lowered.info.get_let_binding_group(*id); + + let signature = group + .signature + .as_slice() + .iter() + .filter_map(|signature| context.stabilized.syntax_ptr(*signature)); + + let equations = group + .equations + .as_ref() + .iter() + .filter_map(|equation| context.stabilized.syntax_ptr(*equation)); + + return spanning_byte_range(signature.chain(equations)); + } }; let range = pointer.text_range(); @@ -48,7 +85,6 @@ where Some((start, end)) } -/// Returns the byte offset range for the most specific (innermost) error step. pub fn current_offset(check_steps: &[ErrorStep], context: &CheckContext) -> Option<(u32, u32)> where Q: ExternalQueries, diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index 26d13738..b65db37e 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -1,6 +1,7 @@ use checking::CheckedModule; use checking::error::ErrorStep; use indexing::IndexedModule; +use lowering::LoweredModule; use rowan::ast::{AstNode, AstPtr}; use rowan::{NodeOrToken, TextRange}; use stabilizing::StabilizedModule; @@ -59,11 +60,28 @@ fn significant_ranges(node: &SyntaxNode) -> Option { Some(start.cover(end)) } +fn pointers_span(context: &DiagnosticsContext<'_>, iterator: I) -> Option +where + I: IntoIterator, + I::IntoIter: DoubleEndedIterator, +{ + let mut iter = iterator.into_iter(); + + let start_ptr = iter.next()?; + let end_ptr = iter.next_back().unwrap_or(start_ptr); + + let start_span = context.span_from_syntax_ptr(&start_ptr)?; + let end_span = context.span_from_syntax_ptr(&end_ptr)?; + + Some(Span::new(start_span.start, end_span.end)) +} + pub struct DiagnosticsContext<'a> { pub content: &'a str, pub root: &'a SyntaxNode, pub stabilized: &'a StabilizedModule, pub indexed: &'a IndexedModule, + pub lowered: &'a LoweredModule, pub checked: &'a CheckedModule, } @@ -73,9 +91,10 @@ impl<'a> DiagnosticsContext<'a> { root: &'a SyntaxNode, stabilized: &'a StabilizedModule, indexed: &'a IndexedModule, + lowered: &'a LoweredModule, checked: &'a CheckedModule, ) -> DiagnosticsContext<'a> { - DiagnosticsContext { content, root, stabilized, indexed, checked } + DiagnosticsContext { content, root, stabilized, indexed, lowered, checked } } pub fn span_from_syntax_ptr(&self, ptr: &SyntaxNodePtr) -> Option { @@ -124,6 +143,23 @@ impl<'a> DiagnosticsContext<'a> { ErrorStep::InferringAdoMap(id) | ErrorStep::InferringAdoApply(id) | ErrorStep::CheckingAdoLet(id) => self.stabilized.syntax_ptr(*id)?, + ErrorStep::CheckingLetName(id) => { + let group = self.lowered.info.get_let_binding_group(*id); + + let signature = group + .signature + .as_slice() + .into_iter() + .filter_map(|signature| self.stabilized.syntax_ptr(*signature)); + + let equations = group + .equations + .as_ref() + .iter() + .filter_map(|equation| self.stabilized.syntax_ptr(*equation)); + + return pointers_span(self, signature.chain(equations)); + } }; self.span_from_syntax_ptr(&ptr) } diff --git a/tests-integration/fixtures/checking/041_where_expression/Main.snap b/tests-integration/fixtures/checking/041_where_expression/Main.snap index 26775003..6e9dfa12 100644 --- a/tests-integration/fixtures/checking/041_where_expression/Main.snap +++ b/tests-integration/fixtures/checking/041_where_expression/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -10,8 +11,8 @@ compose :: ((t14 :: Type) -> (t12 :: Type)) -> (t14 :: Type) -> (t11 :: Type) -apply :: forall (t19 :: Type). (t19 :: Type) -> (t19 :: Type) -nested :: forall (t24 :: Type). (t24 :: Type) -> (t24 :: Type) +apply :: forall (t20 :: Type). (t20 :: Type) -> (t20 :: Type) +nested :: forall (t25 :: Type). (t25 :: Type) -> (t25 :: Type) multiPattern :: Boolean -> Int factorial :: Int -> Int diff --git a/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap b/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap index c6ee89e2..03fca665 100644 --- a/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap @@ -1,9 +1,10 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms -apply :: forall (t4 :: Type). (t4 :: Type) -> (t4 :: Type) +apply :: forall (t5 :: Type). (t5 :: Type) -> (t5 :: Type) apply' :: forall (b :: Type). (b :: Type) -> (b :: Type) Types diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index f6ecb1ec..fc31e7a0 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -14,20 +14,20 @@ Types Diagnostics error[CannotUnify]: Cannot unify 'Int' with 'String' - --> 8:3..12:9 - | -8 | let f :: Int -> Int - | ^~~~~~~~~~~~~~~~~~~ + --> 10:7..11:16 + | +10 | g :: Int -> String + | ^~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 8:3..12:9 + --> 8:7..9:16 | 8 | let f :: Int -> Int - | ^~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' - --> 17:3..18:9 + --> 17:7..17:14 | 17 | let f x = f - | ^~~~~~~~~~~ + | ^~~~~~~ error[CannotUnify]: Cannot unify 'Int' with 'String' --> 25:8..25:9 | diff --git a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap index c8374cb6..7b65faff 100644 --- a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap @@ -23,7 +23,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 17:3..19:10 + --> 18:5..19:10 | -17 | let - | ^~~ +18 | f :: Int + | ^~~~~~~~ diff --git a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap index 35fefc28..11b69a15 100644 --- a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap @@ -25,7 +25,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 17:3..19:10 + --> 18:5..19:10 | -17 | let - | ^~~ +18 | f :: Int + | ^~~~~~~~ diff --git a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap index 7579c10e..1613ff24 100644 --- a/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/268_let_equation_exhaustive/Main.snap @@ -23,12 +23,12 @@ Maybe = [Representational] Diagnostics warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing - --> 7:3..10:15 + --> 8:5..9:19 | -7 | let - | ^~~ +8 | f :: Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _ - --> 14:3..17:17 + --> 15:5..16:18 | -14 | let - | ^~~ +15 | g :: Maybe Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/274_givens_retained/Main.purs b/tests-integration/fixtures/checking/274_givens_retained/Main.purs new file mode 100644 index 00000000..43266042 --- /dev/null +++ b/tests-integration/fixtures/checking/274_givens_retained/Main.purs @@ -0,0 +1,10 @@ +module Main where + +class Given a where + consume :: a -> a + +testGiven :: forall a. Given a => a -> a +testGiven a = consume a + where + b = consume a + c = consume a diff --git a/tests-integration/fixtures/checking/274_givens_retained/Main.snap b/tests-integration/fixtures/checking/274_givens_retained/Main.snap new file mode 100644 index 00000000..41b4ae1b --- /dev/null +++ b/tests-integration/fixtures/checking/274_givens_retained/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) +testGiven :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) + +Types +Given :: Type -> Constraint + +Classes +class Given (&0 :: Type) diff --git a/tests-integration/fixtures/checking/275_givens_scoped/Main.purs b/tests-integration/fixtures/checking/275_givens_scoped/Main.purs new file mode 100644 index 00000000..dbbdec9d --- /dev/null +++ b/tests-integration/fixtures/checking/275_givens_scoped/Main.purs @@ -0,0 +1,15 @@ +module Main where + +class Given a where + consume :: a -> a + +testGiven :: forall a. Given a => a -> a +testGiven a = consume a + where + -- b's constraint should be valid in b + b :: Given Int => a + b = let consumeInt = consume 42 in a + + -- b's constraint should not leak to c + c :: Int + c = consume 42 diff --git a/tests-integration/fixtures/checking/275_givens_scoped/Main.snap b/tests-integration/fixtures/checking/275_givens_scoped/Main.snap new file mode 100644 index 00000000..961b0fa8 --- /dev/null +++ b/tests-integration/fixtures/checking/275_givens_scoped/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) +testGiven :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) + +Types +Given :: Type -> Constraint + +Classes +class Given (&0 :: Type) + +Diagnostics +error[NoInstanceFound]: No instance found for: Given Int + --> 6:1..6:41 + | +6 | testGiven :: forall a. Given a => a -> a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 120358a7..3c227c35 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -396,7 +396,8 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { let lowered = engine.lowered(id).unwrap(); let resolved = engine.resolved(id).unwrap(); - let context = DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &checked); + let context = + DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &lowered, &checked); let mut all_diagnostics = vec![]; diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 3492baad..04b1dcef 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -561,3 +561,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_272_array_nested_constructor_main() { run_test("272_array_nested_constructor", "Main"); } #[rustfmt::skip] #[test] fn test_273_class_member_instantiation_main() { run_test("273_class_member_instantiation", "Main"); } + +#[rustfmt::skip] #[test] fn test_274_givens_retained_main() { run_test("274_givens_retained", "Main"); } + +#[rustfmt::skip] #[test] fn test_275_givens_scoped_main() { run_test("275_givens_scoped", "Main"); } From 9d0990fbb6050fd28a189c7ee90e3af6a7b5a075 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 16:51:39 +0800 Subject: [PATCH 092/386] Use with_local_givens in derive --- .../checking/src/algorithm/derive.rs | 141 +++++++++--------- .../src/algorithm/derive/contravariant.rs | 2 - .../checking/src/algorithm/derive/eq1.rs | 1 - .../checking/src/algorithm/derive/foldable.rs | 2 - .../checking/src/algorithm/derive/functor.rs | 2 - .../checking/src/algorithm/derive/newtype.rs | 1 - .../checking/src/algorithm/derive/tools.rs | 2 +- .../src/algorithm/derive/traversable.rs | 2 - 8 files changed, 72 insertions(+), 81 deletions(-) diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index c943ffe9..96f6832f 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -42,61 +42,65 @@ pub fn check_derive( where Q: ExternalQueries, { - let CheckDerive { - item_id, - derive_id, - constraints, - arguments, - class_file, - class_id, - is_newtype, - } = input; - - state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { - // Save the current size of the environment for unbinding. - let size = state.type_scope.size(); - - let class_kind = kind::lookup_file_type(state, context, class_file, class_id)?; - let expected_kinds = term_item::instantiate_class_kind(state, context, class_kind)?; - - if expected_kinds.len() != arguments.len() { - state.insert_error(ErrorKind::InstanceHeadMismatch { - class_file, - class_item: class_id, - expected: expected_kinds.len(), - actual: arguments.len(), - }); - } + state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { + state.with_local_givens(|state| check_derive_core(state, context, input)) + }) +} - let mut core_arguments = vec![]; - for (argument, expected_kind) in arguments.iter().zip(expected_kinds) { - let (inferred_type, inferred_kind) = - kind::check_surface_kind(state, context, *argument, expected_kind)?; - core_arguments.push((inferred_type, inferred_kind)); - } +fn check_derive_core( + state: &mut CheckState, + context: &CheckContext, + input: CheckDerive<'_>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let CheckDerive { derive_id, constraints, arguments, class_file, class_id, is_newtype, .. } = + input; - let mut core_constraints = vec![]; - for constraint in constraints.iter() { - let (inferred_type, inferred_kind) = - kind::infer_surface_kind(state, context, *constraint)?; - core_constraints.push((inferred_type, inferred_kind)); - } + // Save the current size of the environment for unbinding. + let size = state.type_scope.size(); - let elaborated = tools::ElaboratedDerive { - derive_id, - constraints: core_constraints, - arguments: core_arguments, + let class_kind = kind::lookup_file_type(state, context, class_file, class_id)?; + let expected_kinds = term_item::instantiate_class_kind(state, context, class_kind)?; + + if expected_kinds.len() != arguments.len() { + state.insert_error(ErrorKind::InstanceHeadMismatch { class_file, - class_id, - }; + class_item: class_id, + expected: expected_kinds.len(), + actual: arguments.len(), + }); + } + + let mut core_arguments = vec![]; + for (argument, expected_kind) in arguments.iter().zip(expected_kinds) { + let (inferred_type, inferred_kind) = + kind::check_surface_kind(state, context, *argument, expected_kind)?; + core_arguments.push((inferred_type, inferred_kind)); + } + + let mut core_constraints = vec![]; + for constraint in constraints.iter() { + let (inferred_type, inferred_kind) = kind::infer_surface_kind(state, context, *constraint)?; + core_constraints.push((inferred_type, inferred_kind)); + } - if is_newtype { - check_newtype_derive(state, context, elaborated)?; - } else { - let class_is = |known| Some((class_file, class_id)) == known; - let known_types = &context.known_types; + let elaborated = tools::ElaboratedDerive { + derive_id, + constraints: core_constraints, + arguments: core_arguments, + class_file, + class_id, + }; - macro_rules! dispatch { + if is_newtype { + check_newtype_derive(state, context, elaborated)?; + } else { + let class_is = |known| Some((class_file, class_id)) == known; + let known_types = &context.known_types; + + macro_rules! dispatch { ($($($known:ident)|+ => $handler:path),+ $(,)?) => { $(if $(class_is(known_types.$known))||+ { $handler(state, context, elaborated)?; @@ -106,28 +110,27 @@ where }; } - dispatch! { - eq | ord => check_derive_class, - functor => functor::check_derive_functor, - bifunctor => functor::check_derive_bifunctor, - contravariant => contravariant::check_derive_contravariant, - profunctor => contravariant::check_derive_profunctor, - foldable => foldable::check_derive_foldable, - bifoldable => foldable::check_derive_bifoldable, - traversable => traversable::check_derive_traversable, - bitraversable => traversable::check_derive_bitraversable, - eq1 => eq1::check_derive_eq1, - ord1 => eq1::check_derive_ord1, - newtype => newtype::check_derive_newtype, - generic => generic::check_derive_generic, - } + dispatch! { + eq | ord => check_derive_class, + functor => functor::check_derive_functor, + bifunctor => functor::check_derive_bifunctor, + contravariant => contravariant::check_derive_contravariant, + profunctor => contravariant::check_derive_profunctor, + foldable => foldable::check_derive_foldable, + bifoldable => foldable::check_derive_bifoldable, + traversable => traversable::check_derive_traversable, + bitraversable => traversable::check_derive_bitraversable, + eq1 => eq1::check_derive_eq1, + ord1 => eq1::check_derive_ord1, + newtype => newtype::check_derive_newtype, + generic => generic::check_derive_generic, } + } - // Unbind type variables bound during elaboration. - state.type_scope.unbind(debruijn::Level(size.0)); + // Unbind type variables bound during elaboration. + state.type_scope.unbind(debruijn::Level(size.0)); - Ok(()) - }) + Ok(()) } fn check_derive_class( @@ -160,7 +163,6 @@ where tools::register_derived_instance(state, context, input); generate_field_constraints(state, context, data_file, data_id, derived_type, class)?; - tools::solve_and_report_constraints(state, context) } @@ -212,7 +214,6 @@ where tools::register_derived_instance(state, context, input); state.constraints.push_wanted(delegate_constraint); - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/contravariant.rs b/compiler-core/checking/src/algorithm/derive/contravariant.rs index 4a7c7c46..be3971dc 100644 --- a/compiler-core/checking/src/algorithm/derive/contravariant.rs +++ b/compiler-core/checking/src/algorithm/derive/contravariant.rs @@ -40,7 +40,6 @@ where let config = VarianceConfig::Single((Variance::Contravariant, contravariant)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } @@ -82,6 +81,5 @@ where ); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/eq1.rs b/compiler-core/checking/src/algorithm/derive/eq1.rs index e59c449e..c98779af 100644 --- a/compiler-core/checking/src/algorithm/derive/eq1.rs +++ b/compiler-core/checking/src/algorithm/derive/eq1.rs @@ -108,6 +108,5 @@ where // Emit the wanted constraint `Eq (Identity a)` let wanted_constraint = state.storage.intern(Type::Application(class_type, applied_type)); state.constraints.push_wanted(wanted_constraint); - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/foldable.rs b/compiler-core/checking/src/algorithm/derive/foldable.rs index 180434db..8995899c 100644 --- a/compiler-core/checking/src/algorithm/derive/foldable.rs +++ b/compiler-core/checking/src/algorithm/derive/foldable.rs @@ -40,7 +40,6 @@ where let config = VarianceConfig::Single((Variance::Covariant, foldable)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } @@ -79,6 +78,5 @@ where VarianceConfig::Pair((Variance::Covariant, foldable), (Variance::Covariant, foldable)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/functor.rs b/compiler-core/checking/src/algorithm/derive/functor.rs index 0123af27..7ee41d1a 100644 --- a/compiler-core/checking/src/algorithm/derive/functor.rs +++ b/compiler-core/checking/src/algorithm/derive/functor.rs @@ -40,7 +40,6 @@ where let config = VarianceConfig::Single((Variance::Covariant, functor)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } @@ -79,6 +78,5 @@ where VarianceConfig::Pair((Variance::Covariant, functor), (Variance::Covariant, functor)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/newtype.rs b/compiler-core/checking/src/algorithm/derive/newtype.rs index 1a217ccd..887c4030 100644 --- a/compiler-core/checking/src/algorithm/derive/newtype.rs +++ b/compiler-core/checking/src/algorithm/derive/newtype.rs @@ -47,6 +47,5 @@ where tools::push_given_constraints(state, &input.constraints); tools::emit_superclass_constraints(state, context, &input)?; tools::register_derived_instance(state, context, input); - tools::solve_and_report_constraints(state, context) } diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 34626635..74614c9c 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -93,7 +93,7 @@ pub fn solve_and_report_constraints( where Q: ExternalQueries, { - let residual = state.solve_constraints(context)?; + let residual = state.solve_constraints_local(context)?; for constraint in residual { let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); diff --git a/compiler-core/checking/src/algorithm/derive/traversable.rs b/compiler-core/checking/src/algorithm/derive/traversable.rs index 05616c81..5d2f6ae6 100644 --- a/compiler-core/checking/src/algorithm/derive/traversable.rs +++ b/compiler-core/checking/src/algorithm/derive/traversable.rs @@ -40,7 +40,6 @@ where let config = VarianceConfig::Single((Variance::Covariant, traversable)); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } @@ -81,6 +80,5 @@ where ); generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) } From a9160771454f4498ad127f5c37a810c818edb07e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 17:00:31 +0800 Subject: [PATCH 093/386] Refactor solve_constraints to require with_local_givens --- .../checking/src/algorithm/derive/tools.rs | 2 +- compiler-core/checking/src/algorithm/state.rs | 13 +- compiler-core/checking/src/algorithm/term.rs | 2 +- .../checking/src/algorithm/term_item.rs | 222 ++++++++++-------- 4 files changed, 126 insertions(+), 113 deletions(-) diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 74614c9c..34626635 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -93,7 +93,7 @@ pub fn solve_and_report_constraints( where Q: ExternalQueries, { - let residual = state.solve_constraints_local(context)?; + let residual = state.solve_constraints(context)?; for constraint in residual { let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::NoInstanceFound { constraint }); diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 1ab3c056..7763fa20 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -998,14 +998,6 @@ impl CheckState { result } - pub fn solve_constraints(&mut self, context: &CheckContext) -> QueryResult> - where - Q: ExternalQueries, - { - let (wanted, given) = self.constraints.take(); - constraint::solve_constraints(self, context, wanted, &given) - } - pub fn with_local_givens(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { let length = self.constraints.given.len(); let result = action(self); @@ -1013,10 +1005,7 @@ impl CheckState { result } - pub fn solve_constraints_local( - &mut self, - context: &CheckContext, - ) -> QueryResult> + pub fn solve_constraints(&mut self, context: &CheckContext) -> QueryResult> where Q: ExternalQueries, { diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 8bc1b224..b2ef288b 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -2005,7 +2005,7 @@ where // to the parent scope's wanted constraints. Given constraints must // also be preserved across let bindings. This is demonstrated by // 274_givens_retained, 275_givens_scoped - let residual = state.solve_constraints_local(context)?; + let residual = state.solve_constraints(context)?; state.constraints.extend_wanted(&residual); Ok(()) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index a34eeef0..99391392 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -270,31 +270,44 @@ pub fn check_value_group( context: &CheckContext, input: CheckValueGroup<'_>, ) -> QueryResult> +where + Q: ExternalQueries, +{ + state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { + state.with_local_givens(|state| { + let _span = tracing::debug_span!("check_value_group").entered(); + check_value_group_core(context, state, input) + }) + }) +} + +fn check_value_group_core( + context: &CheckContext, + state: &mut CheckState, + input: CheckValueGroup<'_>, +) -> QueryResult> where Q: ExternalQueries, { let CheckValueGroup { item_id, signature, equations } = input; - state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { - let _span = tracing::debug_span!("check_value_group").entered(); - if let Some(signature_id) = signature { - let group_type = term::lookup_file_term(state, context, context.id, item_id)?; + if let Some(signature_id) = signature { + let group_type = term::lookup_file_term(state, context, context.id, item_id)?; - let surface_bindings = state.surface_bindings.get_term(item_id); - let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); + let surface_bindings = state.surface_bindings.get_term(item_id); + let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); - let signature = - inspect::inspect_signature_core(state, context, group_type, surface_bindings)?; + let signature = + inspect::inspect_signature_core(state, context, group_type, surface_bindings)?; - term::check_equations(state, context, *signature_id, signature, equations)?; - crate::debug_fields!(state, context, { group_type = group_type }, "checking"); - Ok(None) - } else { - let (inferred_type, residual_constraints) = - term::infer_equations(state, context, item_id, equations)?; - crate::debug_fields!(state, context, { inferred_type = inferred_type }, "inferring"); - Ok(Some(InferredValueGroup { inferred_type, residual_constraints })) - } - }) + term::check_equations(state, context, *signature_id, signature, equations)?; + crate::debug_fields!(state, context, { group_type = group_type }, "checked"); + Ok(None) + } else { + let (inferred_type, residual_constraints) = + term::infer_equations(state, context, item_id, equations)?; + crate::debug_fields!(state, context, { inferred_type = inferred_type }, "inferred"); + Ok(Some(InferredValueGroup { inferred_type, residual_constraints })) + } } /// Generalises an [`InferredValueGroup`]. @@ -448,11 +461,23 @@ pub fn check_instance_member_group( context: &CheckContext, input: CheckInstanceMemberGroup<'_>, ) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_error_step(ErrorStep::TermDeclaration(input.instance_id), |state| { + state.with_local_givens(|state| check_instance_member_group_core(state, context, input)) + }) +} + +fn check_instance_member_group_core( + state: &mut CheckState, + context: &CheckContext, + input: CheckInstanceMemberGroup<'_>, +) -> QueryResult<()> where Q: ExternalQueries, { let CheckInstanceMemberGroup { - instance_id, instance_bindings, member, class_file, @@ -460,105 +485,104 @@ where instance_arguments, instance_constraints, kind_variables, + .. } = input; - state.with_error_step(ErrorStep::TermDeclaration(instance_id), |state| { - let _span = tracing::debug_span!("check_instance_member_group").entered(); - - // Save the current size of the environment for unbinding. - let size = state.type_scope.size(); + let _span = tracing::debug_span!("check_instance_member_group").entered(); - // Bind kind variables generalised after instance head checking. - for &kind_variable in kind_variables { - let kind = transfer::localize(state, context, kind_variable); - state.type_scope.bind_core(kind); - } + // Save the current size of the environment for unbinding. + let size = state.type_scope.size(); - for binding in instance_bindings { - state.type_scope.bind_implicit(binding.node, binding.id, binding.kind); - } + // Bind kind variables generalised after instance head checking. + for &kind_variable in kind_variables { + let kind = transfer::localize(state, context, kind_variable); + state.type_scope.bind_core(kind); + } - let class_member_type = lookup_class_member(state, context, member.resolution)?; + for binding in instance_bindings { + state.type_scope.bind_implicit(binding.node, binding.id, binding.kind); + } - for (constraint_type, _) in instance_constraints { - let local_constraint = transfer::localize(state, context, *constraint_type); - state.constraints.push_given(local_constraint); - } + let class_member_type = lookup_class_member(state, context, member.resolution)?; - let specialized_type = if let Some(class_member_type) = class_member_type { - specialize_class_member( - state, - context, - class_member_type, - (class_file, class_id), - instance_arguments, - )? - } else { - None - }; + for (constraint_type, _) in instance_constraints { + let local_constraint = transfer::localize(state, context, *constraint_type); + state.constraints.push_given(local_constraint); + } - // The specialized type may have constraints like `Show a => (a -> b) -> f a -> f b`. - // We push `Show a` as a given and use the body `(a -> b) -> f a -> f b` for checking. - let specialized_type = specialized_type.map(|mut t| { - while let normalized = state.normalize_type(t) - && let Type::Constrained(constraint, constrained) = &state.storage[normalized] - { - state.constraints.push_given(*constraint); - t = *constrained; - } - t - }); + let specialized_type = if let Some(class_member_type) = class_member_type { + specialize_class_member( + state, + context, + class_member_type, + (class_file, class_id), + instance_arguments, + )? + } else { + None + }; - if let Some(signature_id) = &member.signature { - let surface_bindings = inspect::collect_signature_variables(context, *signature_id); + // The specialized type may have constraints like `Show a => (a -> b) -> f a -> f b`. + // We push `Show a` as a given and use the body `(a -> b) -> f a -> f b` for checking. + let specialized_type = specialized_type.map(|mut t| { + while let normalized = state.normalize_type(t) + && let Type::Constrained(constraint, constrained) = &state.storage[normalized] + { + state.constraints.push_given(*constraint); + t = *constrained; + } + t + }); - let (member_type, _) = - kind::check_surface_kind(state, context, *signature_id, context.prim.t)?; + if let Some(signature_id) = &member.signature { + let surface_bindings = inspect::collect_signature_variables(context, *signature_id); - if let Some(specialized_type) = specialized_type { - let unified = unification::unify(state, context, member_type, specialized_type)?; - if !unified { - let expected = state.render_local_type(context, specialized_type); - let actual = state.render_local_type(context, member_type); - state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); - } - } + let (member_type, _) = + kind::check_surface_kind(state, context, *signature_id, context.prim.t)?; - let signature = - inspect::inspect_signature_core(state, context, member_type, &surface_bindings)?; - - term::check_equations(state, context, *signature_id, signature, &member.equations)?; - } else if let Some(specialized_type) = specialized_type { - let inferred_type = state.fresh_unification_type(context); - term::infer_equations_core(state, context, inferred_type, &member.equations)?; - - let (pattern_types, _) = toolkit::extract_function_arguments(state, specialized_type); - let exhaustiveness = exhaustiveness::check_equation_patterns( - state, - context, - &pattern_types, - &member.equations, - )?; - state.report_exhaustiveness(exhaustiveness); - - let matches = unification::subtype(state, context, inferred_type, specialized_type)?; - if !matches { + if let Some(specialized_type) = specialized_type { + let unified = unification::unify(state, context, member_type, specialized_type)?; + if !unified { let expected = state.render_local_type(context, specialized_type); - let actual = state.render_local_type(context, inferred_type); + let actual = state.render_local_type(context, member_type); state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } + } - let residual = state.solve_constraints(context)?; - for constraint in residual { - let constraint = state.render_local_type(context, constraint); - state.insert_error(ErrorKind::NoInstanceFound { constraint }); - } + let signature = + inspect::inspect_signature_core(state, context, member_type, &surface_bindings)?; + + term::check_equations(state, context, *signature_id, signature, &member.equations)?; + } else if let Some(specialized_type) = specialized_type { + let inferred_type = state.fresh_unification_type(context); + term::infer_equations_core(state, context, inferred_type, &member.equations)?; + + let (pattern_types, _) = toolkit::extract_function_arguments(state, specialized_type); + let exhaustiveness = exhaustiveness::check_equation_patterns( + state, + context, + &pattern_types, + &member.equations, + )?; + state.report_exhaustiveness(exhaustiveness); + + let matches = unification::subtype(state, context, inferred_type, specialized_type)?; + if !matches { + let expected = state.render_local_type(context, specialized_type); + let actual = state.render_local_type(context, inferred_type); + state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } - state.type_scope.unbind(debruijn::Level(size.0)); + let residual = state.solve_constraints(context)?; + for constraint in residual { + let constraint = state.render_local_type(context, constraint); + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + } - Ok(()) - }) + state.type_scope.unbind(debruijn::Level(size.0)); + + Ok(()) } macro_rules! debug_assert_class_constraint { From 7127166bb5edcf1da1321455a297a4dc9ef0c560 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 17:24:18 +0800 Subject: [PATCH 094/386] Move equation checking rules to dedicated submodule --- compiler-core/checking/src/algorithm.rs | 3 + .../checking/src/algorithm/equation.rs | 222 +++++++++++ compiler-core/checking/src/algorithm/term.rs | 366 ++++-------------- .../checking/src/algorithm/term_item.rs | 12 +- 4 files changed, 306 insertions(+), 297 deletions(-) create mode 100644 compiler-core/checking/src/algorithm/equation.rs diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 41e175c7..6004cbff 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -47,6 +47,9 @@ pub mod substitute; /// Implements type inference and checking for [`lowering::ExpressionKind`]. pub mod term; +/// Implements equation checking and inference shared by value and let bindings. +pub mod equation; + /// Shared utilities for common type manipulation patterns. pub mod toolkit; diff --git a/compiler-core/checking/src/algorithm/equation.rs b/compiler-core/checking/src/algorithm/equation.rs new file mode 100644 index 00000000..fa2763ec --- /dev/null +++ b/compiler-core/checking/src/algorithm/equation.rs @@ -0,0 +1,222 @@ +use building_types::QueryResult; +use indexing::TermItemId; + +use crate::ExternalQueries; +use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::{binder, exhaustiveness, inspect, term, toolkit, unification}; +use crate::core::{Type, TypeId}; +use crate::error::ErrorKind; + +/// Infers the type of top-level value group equations. +/// +/// This function depends on the unification variable created for the current +/// binding group by [`CheckState::with_term_group`]. This function returns +/// the inferred type and residual constraints for later generalisation via +/// [`term_item::commit_value_group`]. +/// +/// [`term_item::commit_value_group`]: crate::algorithm::term_item::commit_value_group +pub fn infer_equations( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + equations: &[lowering::Equation], +) -> QueryResult<(TypeId, Vec)> +where + Q: ExternalQueries, +{ + let group_type = state + .binding_group + .lookup_term(item_id) + .expect("invariant violated: invalid binding_group in type inference"); + + infer_equations_core(state, context, group_type, equations)?; + + let (pattern_types, _) = toolkit::extract_function_arguments(state, group_type); + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &pattern_types, equations)?; + state.report_exhaustiveness(exhaustiveness); + + let residual_constraints = state.solve_constraints(context)?; + Ok((group_type, residual_constraints)) +} + +/// Infers the type of value group equations. +/// +/// This function infers the type of each value equation, and then checks +/// that it's a subtype of the provided `group_type`. The `group_type` is +/// usually a unification variable. +/// +/// This function is used to implement inference for the following: +/// - [`lowering::TermItemIr::ValueGroup`] +/// - [`lowering::LetBindingNameGroup`] +/// - [`lowering::InstanceMemberGroup`] +pub(crate) fn infer_equations_core( + state: &mut CheckState, + context: &CheckContext, + group_type: TypeId, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let minimum_equation_arity = + equations.iter().map(|equation| equation.binders.len()).min().unwrap_or(0); + + for equation in equations { + let mut argument_types = vec![]; + for &binder_id in equation.binders.iter() { + let argument_type = binder::infer_binder(state, context, binder_id)?; + argument_types.push(argument_type); + } + + let result_type = state.fresh_unification_type(context); + + // Only use the minimum number of binders across equations. + let argument_types = &argument_types[..minimum_equation_arity]; + let equation_type = state.make_function(argument_types, result_type); + let _ = unification::subtype(state, context, equation_type, group_type)?; + + if let Some(guarded) = &equation.guarded { + let inferred_type = term::infer_guarded_expression(state, context, guarded)?; + let _ = unification::subtype(state, context, inferred_type, result_type)?; + } + } + + Ok(()) +} + +/// Checks the type of value group equations. +/// +/// This function checks each value equation against the signature previously +/// checked by the [`check_term_signature`] and [`inspect_signature_core`] +/// functions. +/// +/// This function depends on a couple of side-effects produced by the +/// [`inspect_signature_core`] function. Type variables that appear in the +/// signature are made visible through rebinding, and given constraints +/// are pushed onto the environment. See the implementation for more details. +/// +/// This function solves all constraints during checking using the +/// [`CheckState::solve_constraints`] function, and reports residual +/// constraints as [`ErrorKind::NoInstanceFound`] errors. +/// +/// [`check_term_signature`]: crate::algorithm::term_item::check_term_signature +/// [`inspect_signature_core`]: crate::algorithm::inspect::inspect_signature_core +pub fn check_equations( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + signature: inspect::InspectSignature, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + check_equations_core(state, context, signature_id, &signature, equations)?; + + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; + state.report_exhaustiveness(exhaustiveness); + + let residual = state.solve_constraints(context)?; + for constraint in residual { + let constraint = state.render_local_type(context, constraint); + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + + if let Some(variable) = signature.variables.first() { + state.type_scope.unbind(variable.level); + } + + Ok(()) +} + +pub(crate) fn check_equations_core( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + signature: &inspect::InspectSignature, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let expected_arity = signature.arguments.len(); + + for equation in equations { + let equation_arity = equation.binders.len(); + + if equation_arity > expected_arity { + let expected = expected_arity as u32; + let actual = equation_arity as u32; + state.insert_error(ErrorKind::TooManyBinders { + signature: signature_id, + expected, + actual, + }); + } + + for (&binder_id, &argument_type) in equation.binders.iter().zip(&signature.arguments) { + let _ = binder::check_argument_binder(state, context, binder_id, argument_type)?; + } + + if equation_arity > expected_arity { + let extra_binders = &equation.binders[expected_arity..]; + for &binder_id in extra_binders { + let _ = binder::infer_binder(state, context, binder_id)?; + } + } + + // Compute expected result type based on how many binders there + // are on each equation, wrapping remaining arguments if partial. + // + // foo :: forall a. a -> a -> Int + // foo = \a b -> a + b + // foo a = \b -> a + b + // foo a b = a + b + // + // signature.arguments := [a, a] + // signature.result := Int + // + // expected_type := + // 0 binders := forall a. a -> a -> Int + // 1 binder := a -> Int + // 2 binders := Int + // + // This matters for type synonyms that expand to functions. The + // return type synonym introduces hidden function arrows that + // increase the expected arity after expansion. + // + // type ReturnsInt a = a -> Int + // + // bar :: forall a. ReturnsInt a -> ReturnsInt a + // bar = \f -> f + // bar f = f + // bar f a = f a + // + // signature.arguments := [ReturnsInt a, a] + // signature.result := Int + // + // expected_type := + // 0 binders := forall a. ReturnsInt a -> ReturnsInt a + // 1 binder := ReturnsInt a + // 2 binders := Int + let expected_type = if equation_arity == 0 { + signature.function + } else if equation_arity >= expected_arity { + signature.result + } else { + let remaining_arguments = &signature.arguments[equation_arity..]; + remaining_arguments.iter().rfold(signature.result, |result, &argument| { + state.storage.intern(Type::Function(argument, result)) + }) + }; + + if let Some(guarded) = &equation.guarded { + let inferred_type = term::infer_guarded_expression(state, context, guarded)?; + let _ = unification::subtype(state, context, inferred_type, expected_type)?; + } + } + + Ok(()) +} diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index b2ef288b..13ee82bb 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1,7 +1,6 @@ use std::iter; use building_types::QueryResult; -use indexing::TermItemId; use itertools::{Itertools, Position}; use smol_str::SmolStr; @@ -9,295 +8,12 @@ use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{ - binder, exhaustiveness, inspect, kind, operator, substitute, toolkit, transfer, unification, + binder, equation, exhaustiveness, inspect, kind, operator, substitute, toolkit, transfer, + unification, }; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::{ErrorKind, ErrorStep}; -/// Infers the type of top-level value group equations. -/// -/// This function depends on the unification variable created for the current -/// binding group by [`CheckState::with_term_group`]. This function returns -/// the inferred type and residual constraints for later generalisation via -/// [`term_item::commit_value_group`]. -/// -/// [`term_item::commit_value_group`]: crate::algorithm::term_item::commit_value_group -pub fn infer_equations( - state: &mut CheckState, - context: &CheckContext, - item_id: TermItemId, - equations: &[lowering::Equation], -) -> QueryResult<(TypeId, Vec)> -where - Q: ExternalQueries, -{ - let group_type = state - .binding_group - .lookup_term(item_id) - .expect("invariant violated: invalid binding_group in type inference"); - - infer_equations_core(state, context, group_type, equations)?; - - let (pattern_types, _) = toolkit::extract_function_arguments(state, group_type); - let exhaustiveness = - exhaustiveness::check_equation_patterns(state, context, &pattern_types, equations)?; - state.report_exhaustiveness(exhaustiveness); - - let residual_constraints = state.solve_constraints(context)?; - Ok((group_type, residual_constraints)) -} - -/// Infers the type of value group equations. -/// -/// This function infers the type of each value equation, and then checks -/// that it's a subtype of the provided `group_type`. The `group_type` is -/// usually a unification variable. -/// -/// This function is used to implement inference for the following: -/// - [`lowering::TermItemIr::ValueGroup`] -/// - [`lowering::LetBindingNameGroup`] -/// - [`lowering::InstanceMemberGroup`] -pub fn infer_equations_core( - state: &mut CheckState, - context: &CheckContext, - group_type: TypeId, - equations: &[lowering::Equation], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let minimum_equation_arity = - equations.iter().map(|equation| equation.binders.len()).min().unwrap_or(0); - - for equation in equations { - let mut argument_types = vec![]; - for &binder_id in equation.binders.iter() { - let argument_type = binder::infer_binder(state, context, binder_id)?; - argument_types.push(argument_type); - } - - let result_type = state.fresh_unification_type(context); - - // Only use the minimum number of binders across equations. - let argument_types = &argument_types[..minimum_equation_arity]; - let equation_type = state.make_function(argument_types, result_type); - let _ = unification::subtype(state, context, equation_type, group_type)?; - - if let Some(guarded) = &equation.guarded { - let inferred_type = infer_guarded_expression(state, context, guarded)?; - let _ = unification::subtype(state, context, inferred_type, result_type)?; - } - } - - Ok(()) -} - -/// Checks the type of value group equations. -/// -/// This function checks each value equation against the signature previously -/// checked by the [`check_term_signature`] and [`inspect_signature_core`] -/// functions. -/// -/// This function depends on a couple of side-effects produced by the -/// [`inspect_signature_core`] function. Type variables that appear in the -/// signature are made visible through rebinding, and given constraints -/// are pushed onto the environment. See the implementation for more details. -/// -/// This function solves all constraints during checking using the -/// [`CheckState::solve_constraints`] function, and reports residual -/// constraints as [`ErrorKind::NoInstanceFound`] errors. -/// -/// [`check_term_signature`]: crate::algorithm::term_item::check_term_signature -/// [`inspect_signature_core`]: crate::algorithm::inspect::inspect_signature_core -pub fn check_equations( - state: &mut CheckState, - context: &CheckContext, - signature_id: lowering::TypeId, - signature: inspect::InspectSignature, - equations: &[lowering::Equation], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - check_equations_core(state, context, signature_id, &signature, equations)?; - - let exhaustiveness = - exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; - state.report_exhaustiveness(exhaustiveness); - - let residual = state.solve_constraints(context)?; - for constraint in residual { - let constraint = state.render_local_type(context, constraint); - state.insert_error(ErrorKind::NoInstanceFound { constraint }); - } - - if let Some(variable) = signature.variables.first() { - state.type_scope.unbind(variable.level); - } - - Ok(()) -} - -fn check_equations_core( - state: &mut CheckState, - context: &CheckContext, - signature_id: lowering::TypeId, - signature: &inspect::InspectSignature, - equations: &[lowering::Equation], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let expected_arity = signature.arguments.len(); - - for equation in equations { - let equation_arity = equation.binders.len(); - - if equation_arity > expected_arity { - let expected = expected_arity as u32; - let actual = equation_arity as u32; - state.insert_error(ErrorKind::TooManyBinders { - signature: signature_id, - expected, - actual, - }); - } - - for (&binder_id, &argument_type) in equation.binders.iter().zip(&signature.arguments) { - let _ = binder::check_argument_binder(state, context, binder_id, argument_type)?; - } - - if equation_arity > expected_arity { - let extra_binders = &equation.binders[expected_arity..]; - for &binder_id in extra_binders { - let _ = binder::infer_binder(state, context, binder_id)?; - } - } - - // Compute expected result type based on how many binders there - // are on each equation, wrapping remaining arguments if partial. - // - // foo :: forall a. a -> a -> Int - // foo = \a b -> a + b - // foo a = \b -> a + b - // foo a b = a + b - // - // signature.arguments := [a, a] - // signature.result := Int - // - // expected_type := - // 0 binders := forall a. a -> a -> Int - // 1 binder := a -> Int - // 2 binders := Int - // - // This matters for type synonyms that expand to functions. The - // return type synonym introduces hidden function arrows that - // increase the expected arity after expansion. - // - // type ReturnsInt a = a -> Int - // - // bar :: forall a. ReturnsInt a -> ReturnsInt a - // bar = \f -> f - // bar f = f - // bar f a = f a - // - // signature.arguments := [ReturnsInt a, a] - // signature.result := Int - // - // expected_type := - // 0 binders := forall a. ReturnsInt a -> ReturnsInt a - // 1 binder := ReturnsInt a - // 2 binders := Int - let expected_type = if equation_arity == 0 { - signature.function - } else if equation_arity >= expected_arity { - signature.result - } else { - let remaining_arguments = &signature.arguments[equation_arity..]; - remaining_arguments.iter().rfold(signature.result, |result, &argument| { - state.storage.intern(Type::Function(argument, result)) - }) - }; - - if let Some(guarded) = &equation.guarded { - let inferred_type = infer_guarded_expression(state, context, guarded)?; - let _ = unification::subtype(state, context, inferred_type, expected_type)?; - } - } - - Ok(()) -} - -fn infer_guarded_expression( - state: &mut CheckState, - context: &CheckContext, - guarded: &lowering::GuardedExpression, -) -> QueryResult -where - Q: ExternalQueries, -{ - match guarded { - lowering::GuardedExpression::Unconditional { where_expression } => { - let Some(w) = where_expression else { - return Ok(context.prim.unknown); - }; - infer_where_expression(state, context, w) - } - lowering::GuardedExpression::Conditionals { pattern_guarded } => { - let mut inferred_type = context.prim.unknown; - for pattern_guarded in pattern_guarded.iter() { - for pattern_guard in pattern_guarded.pattern_guards.iter() { - check_pattern_guard(state, context, pattern_guard)?; - } - if let Some(w) = &pattern_guarded.where_expression { - inferred_type = infer_where_expression(state, context, w)?; - } - } - Ok(inferred_type) - } - } -} - -fn check_pattern_guard( - state: &mut CheckState, - context: &CheckContext, - guard: &lowering::PatternGuard, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some(expression) = guard.expression else { - return Ok(()); - }; - - let expression_type = infer_expression(state, context, expression)?; - - let Some(binder) = guard.binder else { - return Ok(()); - }; - - let _ = binder::check_binder(state, context, binder, expression_type)?; - - Ok(()) -} - -fn infer_where_expression( - state: &mut CheckState, - context: &CheckContext, - where_expression: &lowering::WhereExpression, -) -> QueryResult -where - Q: ExternalQueries, -{ - check_let_chunks(state, context, &where_expression.bindings)?; - - let Some(expression) = where_expression.expression else { - return Ok(context.prim.unknown); - }; - - infer_expression(state, context, expression) -} - /// Checks the type of an expression. #[tracing::instrument(skip_all, name = "check_expression")] pub fn check_expression( @@ -1863,7 +1579,7 @@ where ) } -fn check_let_chunks( +pub(crate) fn check_let_chunks( state: &mut CheckState, context: &CheckContext, chunks: &[lowering::LetBindingChunk], @@ -1988,13 +1704,11 @@ where let signature = inspect::inspect_signature_core(state, context, name_type, surface_bindings)?; - check_equations_core(state, context, signature_id, &signature, &name.equations)?; - + equation::check_equations_core(state, context, signature_id, &signature, &name.equations)?; let pattern_types = &signature.arguments; exhaustiveness::check_equation_patterns(state, context, pattern_types, &name.equations)? } else { - infer_equations_core(state, context, name_type, &name.equations)?; - + equation::infer_equations_core(state, context, name_type, &name.equations)?; let (pattern_types, _) = toolkit::extract_function_arguments(state, name_type); exhaustiveness::check_equation_patterns(state, context, &pattern_types, &name.equations)? }; @@ -2010,3 +1724,73 @@ where Ok(()) } + +pub fn infer_guarded_expression( + state: &mut CheckState, + context: &CheckContext, + guarded: &lowering::GuardedExpression, +) -> QueryResult +where + Q: ExternalQueries, +{ + match guarded { + lowering::GuardedExpression::Unconditional { where_expression } => { + let Some(w) = where_expression else { + return Ok(context.prim.unknown); + }; + infer_where_expression(state, context, w) + } + lowering::GuardedExpression::Conditionals { pattern_guarded } => { + let mut inferred_type = context.prim.unknown; + for pattern_guarded in pattern_guarded.iter() { + for pattern_guard in pattern_guarded.pattern_guards.iter() { + check_pattern_guard(state, context, pattern_guard)?; + } + if let Some(w) = &pattern_guarded.where_expression { + inferred_type = infer_where_expression(state, context, w)?; + } + } + Ok(inferred_type) + } + } +} + +fn check_pattern_guard( + state: &mut CheckState, + context: &CheckContext, + guard: &lowering::PatternGuard, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(expression) = guard.expression else { + return Ok(()); + }; + + let expression_type = infer_expression(state, context, expression)?; + + let Some(binder) = guard.binder else { + return Ok(()); + }; + + let _ = binder::check_binder(state, context, binder, expression_type)?; + + Ok(()) +} + +pub fn infer_where_expression( + state: &mut CheckState, + context: &CheckContext, + where_expression: &lowering::WhereExpression, +) -> QueryResult +where + Q: ExternalQueries, +{ + check_let_chunks(state, context, &where_expression.bindings)?; + + let Some(expression) = where_expression.expression else { + return Ok(context.prim.unknown); + }; + + infer_expression(state, context, expression) +} diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 99391392..25ae57ee 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -10,8 +10,8 @@ use crate::ExternalQueries; use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; use crate::algorithm::{ - constraint, exhaustiveness, inspect, kind, quantify, substitute, term, toolkit, transfer, - unification, + constraint, equation, exhaustiveness, inspect, kind, quantify, substitute, term, toolkit, + transfer, unification, }; use crate::core::{Instance, InstanceKind, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -299,12 +299,12 @@ where let signature = inspect::inspect_signature_core(state, context, group_type, surface_bindings)?; - term::check_equations(state, context, *signature_id, signature, equations)?; + equation::check_equations(state, context, *signature_id, signature, equations)?; crate::debug_fields!(state, context, { group_type = group_type }, "checked"); Ok(None) } else { let (inferred_type, residual_constraints) = - term::infer_equations(state, context, item_id, equations)?; + equation::infer_equations(state, context, item_id, equations)?; crate::debug_fields!(state, context, { inferred_type = inferred_type }, "inferred"); Ok(Some(InferredValueGroup { inferred_type, residual_constraints })) } @@ -552,10 +552,10 @@ where let signature = inspect::inspect_signature_core(state, context, member_type, &surface_bindings)?; - term::check_equations(state, context, *signature_id, signature, &member.equations)?; + equation::check_equations(state, context, *signature_id, signature, &member.equations)?; } else if let Some(specialized_type) = specialized_type { let inferred_type = state.fresh_unification_type(context); - term::infer_equations_core(state, context, inferred_type, &member.equations)?; + equation::infer_equations_core(state, context, inferred_type, &member.equations)?; let (pattern_types, _) = toolkit::extract_function_arguments(state, specialized_type); let exhaustiveness = exhaustiveness::check_equation_patterns( From ce39f9dfb093deccf2add46a526941bb1baa5dc2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 21:34:44 +0800 Subject: [PATCH 095/386] Rename inspect_signature_core to inspect_signature --- compiler-core/checking/src/algorithm/inspect.rs | 2 +- compiler-core/checking/src/algorithm/term.rs | 3 +-- compiler-core/checking/src/algorithm/term_item.rs | 6 ++---- compiler-core/checking/src/algorithm/type_item.rs | 3 +-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index 995d4e76..80cf6a96 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -54,7 +54,7 @@ where Arc::from(variables) } -pub fn inspect_signature_core( +pub fn inspect_signature( state: &mut CheckState, context: &CheckContext, type_id: TypeId, diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 13ee82bb..0d4b340d 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1701,8 +1701,7 @@ where let surface_bindings = state.surface_bindings.get_let(id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); - let signature = - inspect::inspect_signature_core(state, context, name_type, surface_bindings)?; + let signature = inspect::inspect_signature(state, context, name_type, surface_bindings)?; equation::check_equations_core(state, context, signature_id, &signature, &name.equations)?; let pattern_types = &signature.arguments; diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 25ae57ee..7c3d6ee4 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -296,8 +296,7 @@ where let surface_bindings = state.surface_bindings.get_term(item_id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); - let signature = - inspect::inspect_signature_core(state, context, group_type, surface_bindings)?; + let signature = inspect::inspect_signature(state, context, group_type, surface_bindings)?; equation::check_equations(state, context, *signature_id, signature, equations)?; crate::debug_fields!(state, context, { group_type = group_type }, "checked"); @@ -549,8 +548,7 @@ where } } - let signature = - inspect::inspect_signature_core(state, context, member_type, &surface_bindings)?; + let signature = inspect::inspect_signature(state, context, member_type, &surface_bindings)?; equation::check_equations(state, context, *signature_id, signature, &member.equations)?; } else if let Some(specialized_type) = specialized_type { diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index b04d896c..e8b863fb 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -697,8 +697,7 @@ where let surface_bindings = state.surface_bindings.get_type(item_id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); - let signature = - inspect::inspect_signature_core(state, context, stored_kind, surface_bindings)?; + let signature = inspect::inspect_signature(state, context, stored_kind, surface_bindings)?; if variables.len() != signature.arguments.len() { state.insert_error(ErrorKind::TypeSignatureVariableMismatch { From 8aec862d22c47b8af2206779fc523345613b5b0f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Feb 2026 21:47:21 +0800 Subject: [PATCH 096/386] Fix missing unbind in checking rule for let --- compiler-core/checking/src/algorithm/term.rs | 25 +++++++++++++++---- .../checking/041_where_expression/Main.snap | 4 +-- .../checking/042_where_polymorphic/Main.snap | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 0d4b340d..365b4ea7 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1697,23 +1697,38 @@ where return Ok(()); }; - let exhaustiveness = if let Some(signature_id) = name.signature { + if let Some(signature_id) = name.signature { let surface_bindings = state.surface_bindings.get_let(id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); let signature = inspect::inspect_signature(state, context, name_type, surface_bindings)?; equation::check_equations_core(state, context, signature_id, &signature, &name.equations)?; + let pattern_types = &signature.arguments; - exhaustiveness::check_equation_patterns(state, context, pattern_types, &name.equations)? + let exhaustiveness = exhaustiveness::check_equation_patterns( + state, + context, + pattern_types, + &name.equations, + )?; + state.report_exhaustiveness(exhaustiveness); + + if let Some(variable) = signature.variables.first() { + state.type_scope.unbind(variable.level); + } } else { equation::infer_equations_core(state, context, name_type, &name.equations)?; let (pattern_types, _) = toolkit::extract_function_arguments(state, name_type); - exhaustiveness::check_equation_patterns(state, context, &pattern_types, &name.equations)? + let exhaustiveness = exhaustiveness::check_equation_patterns( + state, + context, + &pattern_types, + &name.equations, + )?; + state.report_exhaustiveness(exhaustiveness); }; - state.report_exhaustiveness(exhaustiveness); - // PureScript does not have let generalisation; residuals are moved // to the parent scope's wanted constraints. Given constraints must // also be preserved across let bindings. This is demonstrated by diff --git a/tests-integration/fixtures/checking/041_where_expression/Main.snap b/tests-integration/fixtures/checking/041_where_expression/Main.snap index 6e9dfa12..27e17512 100644 --- a/tests-integration/fixtures/checking/041_where_expression/Main.snap +++ b/tests-integration/fixtures/checking/041_where_expression/Main.snap @@ -11,8 +11,8 @@ compose :: ((t14 :: Type) -> (t12 :: Type)) -> (t14 :: Type) -> (t11 :: Type) -apply :: forall (t20 :: Type). (t20 :: Type) -> (t20 :: Type) -nested :: forall (t25 :: Type). (t25 :: Type) -> (t25 :: Type) +apply :: forall (t19 :: Type). (t19 :: Type) -> (t19 :: Type) +nested :: forall (t24 :: Type). (t24 :: Type) -> (t24 :: Type) multiPattern :: Boolean -> Int factorial :: Int -> Int diff --git a/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap b/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap index 03fca665..92a16ba0 100644 --- a/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap +++ b/tests-integration/fixtures/checking/042_where_polymorphic/Main.snap @@ -4,7 +4,7 @@ assertion_line: 28 expression: report --- Terms -apply :: forall (t5 :: Type). (t5 :: Type) -> (t5 :: Type) +apply :: forall (t4 :: Type). (t4 :: Type) -> (t4 :: Type) apply' :: forall (b :: Type). (b :: Type) -> (b :: Type) Types From 42dd6652548ae6049d8f55c522d9c51e86b4756a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 03:11:43 +0800 Subject: [PATCH 097/386] Add utility functions for patterns and constraints --- compiler-core/AGENTS.md | 2 + .../checking/src/algorithm/equation.rs | 85 ++++++++++++++++--- compiler-core/checking/src/algorithm/term.rs | 23 ++--- .../checking/src/algorithm/term_item.rs | 23 ++--- 4 files changed, 87 insertions(+), 46 deletions(-) diff --git a/compiler-core/AGENTS.md b/compiler-core/AGENTS.md index 9517970f..d27bc88e 100644 --- a/compiler-core/AGENTS.md +++ b/compiler-core/AGENTS.md @@ -5,6 +5,8 @@ transparency (editor introspection) and compatibility with query-based increment ### Pipeline Components +The component names listed below are crate names in this workspace. + - **lexing**: tokenization and the layout algorithm - **parsing**: parsing into a rowan-based CST - **syntax**: types for the rowan-based CST diff --git a/compiler-core/checking/src/algorithm/equation.rs b/compiler-core/checking/src/algorithm/equation.rs index fa2763ec..1d01a29e 100644 --- a/compiler-core/checking/src/algorithm/equation.rs +++ b/compiler-core/checking/src/algorithm/equation.rs @@ -7,6 +7,70 @@ use crate::algorithm::{binder, exhaustiveness, inspect, term, toolkit, unificati use crate::core::{Type, TypeId}; use crate::error::ErrorKind; +/// Type origin for the [`patterns`] function. +pub enum ExhaustivenessOrigin<'a> { + /// The types of equation patterns comes from checking. + FromSignature(&'a [TypeId]), + /// The types of equation patterns comes from inference. + FromType(TypeId), +} + +/// Checks and reports exhaustiveness for an equation group. +pub fn patterns( + state: &mut CheckState, + context: &CheckContext, + origin: ExhaustivenessOrigin<'_>, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match origin { + ExhaustivenessOrigin::FromSignature(signature) => { + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, signature, equations)?; + state.report_exhaustiveness(exhaustiveness); + } + ExhaustivenessOrigin::FromType(t) => { + let (arguments, _) = toolkit::extract_function_arguments(state, t); + let exhaustiveness = + exhaustiveness::check_equation_patterns(state, context, &arguments, equations)?; + state.report_exhaustiveness(exhaustiveness); + } + }; + Ok(()) +} + +/// Constraints policy for the [`constraints`] function. +pub enum ConstraintsPolicy { + /// Residual constraints are returned to the caller. + Return, + /// Residual constraints are eagerly reported. + Report, +} + +/// Solves constraints for an equation group. +pub fn constraints( + state: &mut CheckState, + context: &CheckContext, + policy: ConstraintsPolicy, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let residual = state.solve_constraints(context)?; + match policy { + ConstraintsPolicy::Return => Ok(residual), + ConstraintsPolicy::Report => { + for constraint in residual { + let constraint = state.render_local_type(context, constraint); + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + Ok(vec![]) + } + } +} + /// Infers the type of top-level value group equations. /// /// This function depends on the unification variable created for the current @@ -31,13 +95,11 @@ where infer_equations_core(state, context, group_type, equations)?; - let (pattern_types, _) = toolkit::extract_function_arguments(state, group_type); - let exhaustiveness = - exhaustiveness::check_equation_patterns(state, context, &pattern_types, equations)?; - state.report_exhaustiveness(exhaustiveness); + let origin = ExhaustivenessOrigin::FromType(group_type); + patterns(state, context, origin, equations)?; - let residual_constraints = state.solve_constraints(context)?; - Ok((group_type, residual_constraints)) + let residual = constraints(state, context, ConstraintsPolicy::Return)?; + Ok((group_type, residual)) } /// Infers the type of value group equations. @@ -114,15 +176,10 @@ where { check_equations_core(state, context, signature_id, &signature, equations)?; - let exhaustiveness = - exhaustiveness::check_equation_patterns(state, context, &signature.arguments, equations)?; - state.report_exhaustiveness(exhaustiveness); + let origin = ExhaustivenessOrigin::FromSignature(&signature.arguments); + patterns(state, context, origin, equations)?; - let residual = state.solve_constraints(context)?; - for constraint in residual { - let constraint = state.render_local_type(context, constraint); - state.insert_error(ErrorKind::NoInstanceFound { constraint }); - } + let _ = constraints(state, context, ConstraintsPolicy::Report)?; if let Some(variable) = signature.variables.first() { state.type_scope.unbind(variable.level); diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 365b4ea7..486b42fe 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1705,35 +1705,24 @@ where equation::check_equations_core(state, context, signature_id, &signature, &name.equations)?; - let pattern_types = &signature.arguments; - let exhaustiveness = exhaustiveness::check_equation_patterns( - state, - context, - pattern_types, - &name.equations, - )?; - state.report_exhaustiveness(exhaustiveness); + let origin = equation::ExhaustivenessOrigin::FromSignature(&signature.arguments); + equation::patterns(state, context, origin, &name.equations)?; if let Some(variable) = signature.variables.first() { state.type_scope.unbind(variable.level); } } else { equation::infer_equations_core(state, context, name_type, &name.equations)?; - let (pattern_types, _) = toolkit::extract_function_arguments(state, name_type); - let exhaustiveness = exhaustiveness::check_equation_patterns( - state, - context, - &pattern_types, - &name.equations, - )?; - state.report_exhaustiveness(exhaustiveness); + + let origin = equation::ExhaustivenessOrigin::FromType(name_type); + equation::patterns(state, context, origin, &name.equations)?; }; // PureScript does not have let generalisation; residuals are moved // to the parent scope's wanted constraints. Given constraints must // also be preserved across let bindings. This is demonstrated by // 274_givens_retained, 275_givens_scoped - let residual = state.solve_constraints(context)?; + let residual = equation::constraints(state, context, equation::ConstraintsPolicy::Return)?; state.constraints.extend_wanted(&residual); Ok(()) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 7c3d6ee4..c95fac1f 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -10,8 +10,7 @@ use crate::ExternalQueries; use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; use crate::algorithm::{ - constraint, equation, exhaustiveness, inspect, kind, quantify, substitute, term, toolkit, - transfer, unification, + constraint, equation, inspect, kind, quantify, substitute, term, transfer, unification, }; use crate::core::{Instance, InstanceKind, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -555,14 +554,8 @@ where let inferred_type = state.fresh_unification_type(context); equation::infer_equations_core(state, context, inferred_type, &member.equations)?; - let (pattern_types, _) = toolkit::extract_function_arguments(state, specialized_type); - let exhaustiveness = exhaustiveness::check_equation_patterns( - state, - context, - &pattern_types, - &member.equations, - )?; - state.report_exhaustiveness(exhaustiveness); + let origin = equation::ExhaustivenessOrigin::FromType(specialized_type); + equation::patterns(state, context, origin, &member.equations)?; let matches = unification::subtype(state, context, inferred_type, specialized_type)?; if !matches { @@ -571,11 +564,11 @@ where state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } - let residual = state.solve_constraints(context)?; - for constraint in residual { - let constraint = state.render_local_type(context, constraint); - state.insert_error(ErrorKind::NoInstanceFound { constraint }); - } + let _ = equation::constraints( + state, + context, + equation::ConstraintsPolicy::Report, + )?; } state.type_scope.unbind(debruijn::Level(size.0)); From e8a24408f55a3623cf503ef51bb2238275fdb1ce Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 04:08:37 +0800 Subject: [PATCH 098/386] Avoid consuming surface bindings for generalised bindings --- compiler-core/checking/src/algorithm/inspect.rs | 11 +++++++++-- compiler-core/checking/src/algorithm/kind.rs | 2 +- .../checking/src/algorithm/quantify.rs | 2 +- .../checking/src/algorithm/type_item.rs | 4 ++-- compiler-core/checking/src/core.rs | 1 + .../276_where_clause_outer_scope/Main.purs | 17 +++++++++++++++++ .../276_where_clause_outer_scope/Main.snap | 14 ++++++++++++++ tests-integration/tests/checking.rs | 6 +++--- tests-integration/tests/checking/generated.rs | 2 ++ 9 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.purs create mode 100644 tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.snap diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index 80cf6a96..52522349 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -80,8 +80,15 @@ where Type::Forall(ref binder, inner) => { let mut binder = binder.clone(); - let new_level = if let Some(&binding_id) = surface_bindings.next() { - state.type_scope.bound.bind(debruijn::Variable::Forall(binding_id)) + // Only consume a surface binding for explicit foralls; + // implicit foralls are added during generalisation and + // don't have corresponding surface bindings. + let new_level = if !binder.implicit { + if let Some(&binding_id) = surface_bindings.next() { + state.type_scope.bound.bind(debruijn::Variable::Forall(binding_id)) + } else { + state.type_scope.bound.bind(debruijn::Variable::Core) + } } else { state.type_scope.bound.bind(debruijn::Variable::Core) }; diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index b3825084..45bf82c4 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -638,7 +638,7 @@ where }; let level = state.type_scope.bind_forall(binding.id, kind); - Ok(ForallBinder { visible, name, level, kind }) + Ok(ForallBinder { visible, implicit: false, name, level, kind }) } pub(crate) fn lookup_file_type( diff --git a/compiler-core/checking/src/algorithm/quantify.rs b/compiler-core/checking/src/algorithm/quantify.rs index 19fa52f5..b64ba1bf 100644 --- a/compiler-core/checking/src/algorithm/quantify.rs +++ b/compiler-core/checking/src/algorithm/quantify.rs @@ -44,7 +44,7 @@ pub fn quantify(state: &mut CheckState, id: TypeId) -> Option<(TypeId, debruijn: .to_level(size) .unwrap_or_else(|| unreachable!("invariant violated: invalid {index} for {size}")); - let binder = ForallBinder { visible: false, name, level, kind }; + let binder = ForallBinder { visible: false, implicit: true, name, level, kind }; quantified = state.storage.intern(Type::Forall(binder, quantified)); substitutions.insert(id, (level, kind)); diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index e8b863fb..e7622f5b 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -749,7 +749,7 @@ where let result_kind = signature.result; let type_variables = kinds.into_iter().map(|(id, visible, name, kind)| { let level = state.type_scope.bind_forall(id, kind); - ForallBinder { visible, name, level, kind } + ForallBinder { visible, implicit: false, name, level, kind } }); let type_variables = type_variables.collect_vec(); @@ -769,7 +769,7 @@ where let visible = variable.visible; let name = variable.name.clone().unwrap_or(MISSING_NAME); let level = state.type_scope.bind_forall(variable.id, kind); - Ok(ForallBinder { visible, name, level, kind }) + Ok(ForallBinder { visible, implicit: false, name, level, kind }) }); let type_variables = type_variables.collect::>>()?; diff --git a/compiler-core/checking/src/core.rs b/compiler-core/checking/src/core.rs index 2c52d099..b43c1d32 100644 --- a/compiler-core/checking/src/core.rs +++ b/compiler-core/checking/src/core.rs @@ -12,6 +12,7 @@ use smol_str::SmolStr; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ForallBinder { pub visible: bool, + pub implicit: bool, pub name: SmolStr, pub level: debruijn::Level, pub kind: TypeId, diff --git a/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.purs b/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.purs new file mode 100644 index 00000000..93ea2854 --- /dev/null +++ b/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Type.Proxy (Proxy(..)) + +-- Minimal reproduction: where clause referencing outer type variables +-- The `r` in coerce's signature should reference the outer forall's `r` + +foreign import unsafeCoerce :: forall a b. a -> b + +test :: forall t r. t -> (forall v. Proxy v -> r) -> r +test s f = coerce f Proxy + where + coerce + :: (forall v. Proxy v -> r) + -> Proxy _ + -> r + coerce = unsafeCoerce diff --git a/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.snap b/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.snap new file mode 100644 index 00000000..33878c7e --- /dev/null +++ b/tests-integration/fixtures/checking/276_where_clause_outer_scope/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +test :: + forall (t6 :: Type) (t :: Type) (r :: Type). + (t :: Type) -> + (forall (v :: (t6 :: Type)). Proxy @(t6 :: Type) (v :: (t6 :: Type)) -> (r :: Type)) -> + (r :: Type) + +Types diff --git a/tests-integration/tests/checking.rs b/tests-integration/tests/checking.rs index 66882f22..c37bdd67 100644 --- a/tests-integration/tests/checking.rs +++ b/tests-integration/tests/checking.rs @@ -285,7 +285,7 @@ fn make_forall_a_to_a(context: &CheckContext, state: &mut CheckStat let bound_a = state.bound_variable(0, context.prim.t); let a_to_a = state.function(bound_a, bound_a); - let binder = ForallBinder { visible: false, name: "a".into(), level, kind: context.prim.t }; + let binder = ForallBinder { visible: false, implicit: false, name: "a".into(), level, kind: context.prim.t }; let forall_a_to_a = state.storage.intern(Type::Forall(binder, a_to_a)); state.type_scope.unbind(level); @@ -352,13 +352,13 @@ fn test_subtype_nested_forall() { let a_to_b_to_a = state.function(bound_a, b_to_a); let forall_b = state.storage.intern(Type::Forall( - ForallBinder { visible: false, name: "b".into(), level: level_b, kind: context.prim.t }, + ForallBinder { visible: false, implicit: false, name: "b".into(), level: level_b, kind: context.prim.t }, a_to_b_to_a, )); state.type_scope.unbind(level_b); let forall_a_b = state.storage.intern(Type::Forall( - ForallBinder { visible: false, name: "a".into(), level: level_a, kind: context.prim.t }, + ForallBinder { visible: false, implicit: false, name: "a".into(), level: level_a, kind: context.prim.t }, forall_b, )); state.type_scope.unbind(level_a); diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 04b1dcef..16f05391 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -565,3 +565,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_274_givens_retained_main() { run_test("274_givens_retained", "Main"); } #[rustfmt::skip] #[test] fn test_275_givens_scoped_main() { run_test("275_givens_scoped", "Main"); } + +#[rustfmt::skip] #[test] fn test_276_where_clause_outer_scope_main() { run_test("276_where_clause_outer_scope", "Main"); } From 92d2db8c4c99348775efabe2b452c9334306171c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 04:10:25 +0800 Subject: [PATCH 099/386] Format files and fix clippy lints --- .../src/algorithm/exhaustiveness/pretty.rs | 4 ++-- .../checking/src/algorithm/term_item.rs | 6 +---- compiler-core/diagnostics/src/context.rs | 2 +- tests-integration/tests/checking.rs | 24 ++++++++++++++++--- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs index e9e17946..6feca0d3 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/pretty.rs @@ -141,7 +141,7 @@ where if *negative { builder.push('-'); } - builder.push_str(&n.to_string()); + builder.push_str(n.as_ref()); builder.finish() } } @@ -162,5 +162,5 @@ where }; let item = &indexed.items[term_id]; - item.name.as_ref().map(SmolStr::clone) + item.name.clone() } diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index c95fac1f..652d9a33 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -564,11 +564,7 @@ where state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } - let _ = equation::constraints( - state, - context, - equation::ConstraintsPolicy::Report, - )?; + let _ = equation::constraints(state, context, equation::ConstraintsPolicy::Report)?; } state.type_scope.unbind(debruijn::Level(size.0)); diff --git a/compiler-core/diagnostics/src/context.rs b/compiler-core/diagnostics/src/context.rs index b65db37e..d9f83923 100644 --- a/compiler-core/diagnostics/src/context.rs +++ b/compiler-core/diagnostics/src/context.rs @@ -149,7 +149,7 @@ impl<'a> DiagnosticsContext<'a> { let signature = group .signature .as_slice() - .into_iter() + .iter() .filter_map(|signature| self.stabilized.syntax_ptr(*signature)); let equations = group diff --git a/tests-integration/tests/checking.rs b/tests-integration/tests/checking.rs index c37bdd67..eab3ed53 100644 --- a/tests-integration/tests/checking.rs +++ b/tests-integration/tests/checking.rs @@ -285,7 +285,13 @@ fn make_forall_a_to_a(context: &CheckContext, state: &mut CheckStat let bound_a = state.bound_variable(0, context.prim.t); let a_to_a = state.function(bound_a, bound_a); - let binder = ForallBinder { visible: false, implicit: false, name: "a".into(), level, kind: context.prim.t }; + let binder = ForallBinder { + visible: false, + implicit: false, + name: "a".into(), + level, + kind: context.prim.t, + }; let forall_a_to_a = state.storage.intern(Type::Forall(binder, a_to_a)); state.type_scope.unbind(level); @@ -352,13 +358,25 @@ fn test_subtype_nested_forall() { let a_to_b_to_a = state.function(bound_a, b_to_a); let forall_b = state.storage.intern(Type::Forall( - ForallBinder { visible: false, implicit: false, name: "b".into(), level: level_b, kind: context.prim.t }, + ForallBinder { + visible: false, + implicit: false, + name: "b".into(), + level: level_b, + kind: context.prim.t, + }, a_to_b_to_a, )); state.type_scope.unbind(level_b); let forall_a_b = state.storage.intern(Type::Forall( - ForallBinder { visible: false, implicit: false, name: "a".into(), level: level_a, kind: context.prim.t }, + ForallBinder { + visible: false, + implicit: false, + name: "a".into(), + level: level_a, + kind: context.prim.t, + }, forall_b, )); state.type_scope.unbind(level_a); From 5a98bf22b646e28f6b012159f92af28fc4ba0e81 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 12:08:14 +0800 Subject: [PATCH 100/386] Support soft keywords in variable positions --- compiler-core/parsing/src/parser/names.rs | 53 ++----------------- compiler-core/syntax/src/cst.rs | 16 ++++-- compiler-core/syntax/src/cst/macros.rs | 15 ++++++ compiler-core/syntax/src/lib.rs | 1 + compiler-core/syntax/src/names.rs | 50 +++++++++++++++++ .../277_keyword_as_variable/Main.purs | 15 ++++++ .../277_keyword_as_variable/Main.snap | 28 ++++++++++ tests-integration/tests/checking/generated.rs | 2 + 8 files changed, 125 insertions(+), 55 deletions(-) create mode 100644 compiler-core/syntax/src/names.rs create mode 100644 tests-integration/fixtures/checking/277_keyword_as_variable/Main.purs create mode 100644 tests-integration/fixtures/checking/277_keyword_as_variable/Main.snap diff --git a/compiler-core/parsing/src/parser/names.rs b/compiler-core/parsing/src/parser/names.rs index e4760058..bc5e6ce9 100644 --- a/compiler-core/parsing/src/parser/names.rs +++ b/compiler-core/parsing/src/parser/names.rs @@ -1,24 +1,8 @@ -use syntax::{SyntaxKind, TokenSet}; +use syntax::SyntaxKind; -use super::Parser; - -pub(super) const LOWER: TokenSet = - TokenSet::new(&[SyntaxKind::LOWER, SyntaxKind::AS, SyntaxKind::HIDING, SyntaxKind::ROLE]) - .union(ROLE); - -pub(super) const ROLE: TokenSet = - TokenSet::new(&[SyntaxKind::NOMINAL, SyntaxKind::PHANTOM, SyntaxKind::REPRESENTATIONAL]); - -pub(super) const OPERATOR: TokenSet = TokenSet::new(&[ - SyntaxKind::OPERATOR, - SyntaxKind::COLON, - SyntaxKind::MINUS, - SyntaxKind::DOUBLE_PERIOD, - SyntaxKind::LEFT_THICK_ARROW, -]); +pub(super) use syntax::names::{KEYWORD, LOWER, OPERATOR, OPERATOR_NAME, RECORD_LABEL, ROLE}; -pub(super) const OPERATOR_NAME: TokenSet = - TokenSet::new(&[SyntaxKind::OPERATOR_NAME, SyntaxKind::DOUBLE_PERIOD_OPERATOR_NAME]); +use super::Parser; pub(super) fn module_name(p: &mut Parser) { p.annotate(); @@ -60,37 +44,6 @@ pub(super) fn operator_name(p: &mut Parser) { m.end(p, SyntaxKind::QualifiedName); } -pub(super) const KEYWORD: TokenSet = TokenSet::new(&[ - SyntaxKind::MODULE, - SyntaxKind::WHERE, - SyntaxKind::IMPORT, - SyntaxKind::ADO, - SyntaxKind::DO, - SyntaxKind::IF, - SyntaxKind::THEN, - SyntaxKind::ELSE, - SyntaxKind::LET, - SyntaxKind::IN, - SyntaxKind::CASE, - SyntaxKind::OF, - SyntaxKind::DATA, - SyntaxKind::NEWTYPE, - SyntaxKind::FORALL, - SyntaxKind::TYPE, - SyntaxKind::CLASS, - SyntaxKind::INSTANCE, - SyntaxKind::DERIVE, - SyntaxKind::FOREIGN, - SyntaxKind::INFIXL, - SyntaxKind::INFIXR, - SyntaxKind::INFIX, - SyntaxKind::TRUE, - SyntaxKind::FALSE, -]); - -pub(super) const RECORD_LABEL: TokenSet = - TokenSet::new(&[SyntaxKind::STRING, SyntaxKind::RAW_STRING]).union(LOWER).union(KEYWORD); - pub(super) fn label(p: &mut Parser) { let mut m = p.start(); p.expect_in(RECORD_LABEL, SyntaxKind::TEXT, "Expected RECORD_LABEL"); diff --git a/compiler-core/syntax/src/cst.rs b/compiler-core/syntax/src/cst.rs index 69f62233..a2faa918 100644 --- a/compiler-core/syntax/src/cst.rs +++ b/compiler-core/syntax/src/cst.rs @@ -1,5 +1,7 @@ use rowan::ast::AstNode; +use crate::names; + #[macro_use] mod macros; @@ -18,11 +20,15 @@ has_token!( has_token!( QualifiedName | upper() -> UPPER - | lower() -> LOWER | operator() -> OPERATOR | operator_name() -> OPERATOR_NAME ); +has_token_set!( + QualifiedName + | lower() -> names::LOWER +); + has_child!( QualifiedName | qualifier() -> Qualifier @@ -987,14 +993,14 @@ has_children!( | children() -> Binder ); -has_token!( +has_token_set!( BinderVariable - | name_token() -> LOWER + | name_token() -> names::LOWER ); -has_token!( +has_token_set!( BinderNamed - | name_token() -> LOWER + | name_token() -> names::LOWER ); has_child!( diff --git a/compiler-core/syntax/src/cst/macros.rs b/compiler-core/syntax/src/cst/macros.rs index 952febd2..90f0ef58 100644 --- a/compiler-core/syntax/src/cst/macros.rs +++ b/compiler-core/syntax/src/cst/macros.rs @@ -97,6 +97,21 @@ macro_rules! has_token { }; } +macro_rules! has_token_set { + ($kind:ident $(|$name:ident() -> $set:expr)+) => { + impl $kind { + $( + pub fn $name(&self) -> Option { + self.syntax() + .children_with_tokens() + .filter_map(|element| element.into_token()) + .find(|token| $set.contains(token.kind())) + } + )+ + } + }; +} + macro_rules! has_tokens { ($kind:ident $(|$name:ident() -> $token:ident)+) => { impl $kind { diff --git a/compiler-core/syntax/src/lib.rs b/compiler-core/syntax/src/lib.rs index 924947bb..a08a4592 100644 --- a/compiler-core/syntax/src/lib.rs +++ b/compiler-core/syntax/src/lib.rs @@ -1,4 +1,5 @@ pub mod cst; +pub mod names; mod token_set; pub use token_set::TokenSet; diff --git a/compiler-core/syntax/src/names.rs b/compiler-core/syntax/src/names.rs new file mode 100644 index 00000000..4b473128 --- /dev/null +++ b/compiler-core/syntax/src/names.rs @@ -0,0 +1,50 @@ +use crate::{SyntaxKind, TokenSet}; + +pub const ROLE: TokenSet = + TokenSet::new(&[SyntaxKind::NOMINAL, SyntaxKind::PHANTOM, SyntaxKind::REPRESENTATIONAL]); + +pub const LOWER: TokenSet = + TokenSet::new(&[SyntaxKind::LOWER, SyntaxKind::AS, SyntaxKind::HIDING, SyntaxKind::ROLE]) + .union(ROLE); + +pub const OPERATOR: TokenSet = TokenSet::new(&[ + SyntaxKind::OPERATOR, + SyntaxKind::COLON, + SyntaxKind::MINUS, + SyntaxKind::DOUBLE_PERIOD, + SyntaxKind::LEFT_THICK_ARROW, +]); + +pub const OPERATOR_NAME: TokenSet = + TokenSet::new(&[SyntaxKind::OPERATOR_NAME, SyntaxKind::DOUBLE_PERIOD_OPERATOR_NAME]); + +pub const KEYWORD: TokenSet = TokenSet::new(&[ + SyntaxKind::MODULE, + SyntaxKind::WHERE, + SyntaxKind::IMPORT, + SyntaxKind::ADO, + SyntaxKind::DO, + SyntaxKind::IF, + SyntaxKind::THEN, + SyntaxKind::ELSE, + SyntaxKind::LET, + SyntaxKind::IN, + SyntaxKind::CASE, + SyntaxKind::OF, + SyntaxKind::DATA, + SyntaxKind::NEWTYPE, + SyntaxKind::FORALL, + SyntaxKind::TYPE, + SyntaxKind::CLASS, + SyntaxKind::INSTANCE, + SyntaxKind::DERIVE, + SyntaxKind::FOREIGN, + SyntaxKind::INFIXL, + SyntaxKind::INFIXR, + SyntaxKind::INFIX, + SyntaxKind::TRUE, + SyntaxKind::FALSE, +]); + +pub const RECORD_LABEL: TokenSet = + TokenSet::new(&[SyntaxKind::STRING, SyntaxKind::RAW_STRING]).union(LOWER).union(KEYWORD); diff --git a/tests-integration/fixtures/checking/277_keyword_as_variable/Main.purs b/tests-integration/fixtures/checking/277_keyword_as_variable/Main.purs new file mode 100644 index 00000000..7448ea78 --- /dev/null +++ b/tests-integration/fixtures/checking/277_keyword_as_variable/Main.purs @@ -0,0 +1,15 @@ +module Main where + +foreign import data Test1 :: forall as. as -> as +foreign import data Test2 :: forall phantom. phantom -> phantom +foreign import data Test3 :: forall nominal. nominal -> nominal +foreign import data Test4 :: forall representational. representational -> representational +foreign import data Test5 :: forall hiding. hiding -> hiding +foreign import data Test6 :: forall role. role -> role + +test1 = \as -> as +test2 = \phantom -> phantom +test3 = \nominal -> nominal +test4 = \representational -> representational +test5 = \hiding -> hiding +test6 = \role -> role diff --git a/tests-integration/fixtures/checking/277_keyword_as_variable/Main.snap b/tests-integration/fixtures/checking/277_keyword_as_variable/Main.snap new file mode 100644 index 00000000..b242a5bf --- /dev/null +++ b/tests-integration/fixtures/checking/277_keyword_as_variable/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test1 :: forall (t8 :: Type). (t8 :: Type) -> (t8 :: Type) +test2 :: forall (t11 :: Type). (t11 :: Type) -> (t11 :: Type) +test3 :: forall (t14 :: Type). (t14 :: Type) -> (t14 :: Type) +test4 :: forall (t17 :: Type). (t17 :: Type) -> (t17 :: Type) +test5 :: forall (t20 :: Type). (t20 :: Type) -> (t20 :: Type) +test6 :: forall (t23 :: Type). (t23 :: Type) -> (t23 :: Type) + +Types +Test1 :: forall (as :: Type). (as :: Type) -> (as :: Type) +Test2 :: forall (phantom :: Type). (phantom :: Type) -> (phantom :: Type) +Test3 :: forall (nominal :: Type). (nominal :: Type) -> (nominal :: Type) +Test4 :: forall (representational :: Type). (representational :: Type) -> (representational :: Type) +Test5 :: forall (hiding :: Type). (hiding :: Type) -> (hiding :: Type) +Test6 :: forall (role :: Type). (role :: Type) -> (role :: Type) + +Roles +Test1 = [Nominal] +Test2 = [Nominal] +Test3 = [Nominal] +Test4 = [Nominal] +Test5 = [Nominal] +Test6 = [Nominal] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 16f05391..a131d973 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -567,3 +567,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_275_givens_scoped_main() { run_test("275_givens_scoped", "Main"); } #[rustfmt::skip] #[test] fn test_276_where_clause_outer_scope_main() { run_test("276_where_clause_outer_scope", "Main"); } + +#[rustfmt::skip] #[test] fn test_277_keyword_as_variable_main() { run_test("277_keyword_as_variable", "Main"); } From d8034f1ac158a9b1665c770da8af40b705a75fd1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 13:06:44 +0800 Subject: [PATCH 101/386] Add test creation and deletion utilities --- .claude/skills/incremental-testing/SKILL.md | 50 ---- .claude/skills/type-checker-tests/SKILL.md | 6 +- .../reference/compiler-scripts.md} | 7 +- Cargo.lock | 18 +- compiler-scripts/Cargo.toml | 1 + compiler-scripts/src/main.rs | 66 +++++- compiler-scripts/src/test_runner.rs | 15 ++ compiler-scripts/src/test_runner/cli.rs | 12 + compiler-scripts/src/test_runner/fixture.rs | 213 ++++++++++++++++++ compiler-scripts/src/test_runner/nextest.rs | 3 + tests-integration/Cargo.toml | 2 +- tests-integration/build.rs | 18 +- 12 files changed, 323 insertions(+), 88 deletions(-) delete mode 100644 .claude/skills/incremental-testing/SKILL.md rename .claude/skills/{compiler-scripts/SKILL.md => type-checker-tests/reference/compiler-scripts.md} (93%) create mode 100644 compiler-scripts/src/test_runner/fixture.rs diff --git a/.claude/skills/incremental-testing/SKILL.md b/.claude/skills/incremental-testing/SKILL.md deleted file mode 100644 index ebdb953e..00000000 --- a/.claude/skills/incremental-testing/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: incremental-testing -description: SINGLE SOURCE OF TRUTH for running compiler integration tests + snapshots. Use for checking/lowering/resolving/lsp. Follow exactly; do not discover tests via filesystem. ---- - -# Compiler Integration Testing (AUTHORITATIVE) - -Make sure to load the compiler-scripts skill for command syntax and flags. - -## NON-NEGOTIABLE RULES (must follow) - -1. You MUST run compiler integration tests ONLY via: - `just t [filters...]` - -2. You MUST NOT perform test discovery by inspecting the filesystem. - Specifically, you MUST NOT `glob`, `find`, or `rg` under: - `tests-integration/fixtures/**` - -3. Filters MUST come ONLY from: - - The user's explicit test id/name/pattern, OR - - The CLI output from a previous `just t ...` run. - Filters MUST NOT come from fixture directory exploration. - -4. STOP CONDITION: If you already know `` and `[filters...]`, - run the `just t ...` command now. Do not gather more context. - -## Decision procedure (do exactly this) - -1. Identify category: one of `c` (checking), `l` (lowering), `r` (resolving), `lsp`. -2. If the user provided any ids/patterns → pass them as space-delimited filters. -3. Else → run the category with no filters. -4. If results are too broad → rerun using ids/patterns copied from CLI output. - -## Quick Examples - -```bash -just t c # Run all checking tests -just t c 101 # Run test 101 in checking -just t c 101 102 pattern # Multiple filters -just t c accept --all # Accept all pending checking snapshots -``` - -## Forbidden (never do this) - -- `rg tests-integration/fixtures` -- `find tests-integration/fixtures` -- `glob tests-integration/fixtures/**` -- "Open fixture files to see what tests exist" -- "List fixture directories to decide which test to run" -- Any form of filesystem exploration to discover tests diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index 4e906761..0a42f19a 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -6,7 +6,7 @@ allowed-tools: Bash(mkdir:*) # Type Checker Integration Tests -Make sure to load the compiler-scripts skill for command syntax and flags. The category is `checking`. +Use the command reference at `reference/compiler-scripts.md` for test runner syntax, snapshot workflows, filters, and trace debugging. The category is `checking`. **Language:** Fixtures use PureScript syntax, not Haskell. @@ -15,10 +15,10 @@ Make sure to load the compiler-scripts skill for command syntax and flags. The c ### 1. Create fixture directory ```bash -mkdir tests-integration/fixtures/checking/{NNN_descriptive_name} +just t checking --create "descriptive name" ``` -Find next number: `ls tests-integration/fixtures/checking/ | tail -5` +The CLI picks the next fixture number and creates the folder. Tests are auto-discovered by `build.rs`. diff --git a/.claude/skills/compiler-scripts/SKILL.md b/.claude/skills/type-checker-tests/reference/compiler-scripts.md similarity index 93% rename from .claude/skills/compiler-scripts/SKILL.md rename to .claude/skills/type-checker-tests/reference/compiler-scripts.md index 79742234..aac183a6 100644 --- a/.claude/skills/compiler-scripts/SKILL.md +++ b/.claude/skills/type-checker-tests/reference/compiler-scripts.md @@ -1,8 +1,3 @@ ---- -name: compiler-scripts -description: Command reference for the compiler test runner CLI. Documents flags, options, and trace analysis. ---- - # Compiler Scripts Command Reference CLI tools in `compiler-scripts/` for running integration tests. @@ -17,6 +12,8 @@ just t --diff [filters...] # Run with full inline diffs just t --count 10 [filters...] # Show more snapshots (default: 3) just t --debug [filters...] # Enable tracing just t --verbose [filters...] # Show test progress +just t --create "name" # Scaffold a new fixture +just t --delete "name" # Dry-run fixture deletion (use --confirm) ``` ### Categories diff --git a/Cargo.lock b/Cargo.lock index c30e0b9f..2e7cdb7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,6 +415,7 @@ version = "0.1.0" dependencies = [ "clap", "console", + "heck", "md-5", "serde", "serde_json", @@ -445,15 +446,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -2563,10 +2555,10 @@ dependencies = [ "analyzer", "async-lsp", "checking", - "convert_case", "diagnostics", "files", "glob", + "heck", "indexing", "insta", "interner", @@ -2860,12 +2852,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.2.2" diff --git a/compiler-scripts/Cargo.toml b/compiler-scripts/Cargo.toml index ed235447..bb2f96d5 100644 --- a/compiler-scripts/Cargo.toml +++ b/compiler-scripts/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] clap = { version = "4", features = ["derive"] } console = "0.15" +heck = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" similar = { version = "2", features = ["inline"] } diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs index e6d7b12f..10733d8f 100644 --- a/compiler-scripts/src/main.rs +++ b/compiler-scripts/src/main.rs @@ -1,6 +1,9 @@ use clap::Parser; +use console::style; + use compiler_scripts::test_runner::{ - CategoryCommand, RunArgs, TestCategory, accept_category, reject_category, run_category, + CategoryCommand, DeleteFixtureOutcome, RunArgs, TestCategory, accept_category, create_fixture, + delete_fixture, reject_category, run_category, }; #[derive(Parser)] @@ -16,6 +19,65 @@ struct Cli { fn main() { let cli = Cli::parse(); + if let Some(name) = &cli.args.create { + if let Err(error) = create_fixture(cli.category, name) { + eprintln!("{}", error); + std::process::exit(1); + } + return; + } + + if let Some(name) = &cli.args.delete { + match delete_fixture(cli.category, name, cli.args.confirm) { + Ok(DeleteFixtureOutcome { fixture_paths, snapshot_paths, confirmed }) => { + if confirmed { + for path in &fixture_paths { + println!( + "{} {}", + style("DELETED").red().bold(), + style(path.display()).cyan() + ); + } + for path in &snapshot_paths { + println!( + "{} {}", + style("DELETED").red().bold(), + style(path.display()).cyan() + ); + } + } else { + println!( + "{} pending deletion(s) in {}", + fixture_paths.len() + snapshot_paths.len(), + style(cli.category.as_str()).cyan() + ); + println!(); + for path in &fixture_paths { + println!(" {}", style(path.display()).dim()); + } + for path in &snapshot_paths { + println!(" {}", style(path.display()).dim()); + } + println!(); + println!( + "To delete, run: {}", + style(format!( + "just t {} --delete \"{}\" --confirm", + cli.category.as_str(), + name + )) + .cyan() + ); + } + } + Err(error) => { + eprintln!("{}", error); + std::process::exit(1); + } + } + return; + } + match &cli.args.command { Some(CategoryCommand::Accept(args)) => { let outcome = accept_category(cli.category, args); @@ -36,4 +98,4 @@ fn main() { } } } -} +} \ No newline at end of file diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs index 356e805e..dc3af4ce 100644 --- a/compiler-scripts/src/test_runner.rs +++ b/compiler-scripts/src/test_runner.rs @@ -1,6 +1,7 @@ mod category; mod cli; mod decision; +mod fixture; mod nextest; mod pending; mod traces; @@ -115,3 +116,17 @@ pub fn reject_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotO SnapshotOutcome { success: result.failed == 0, count: result.rejected } } + +pub use fixture::DeleteFixtureOutcome; + +pub fn create_fixture(category: TestCategory, name: &str) -> Result<(), String> { + fixture::create_fixture(category, name).map(|_| ()) +} + +pub fn delete_fixture( + category: TestCategory, + name: &str, + confirm: bool, +) -> Result { + fixture::delete_fixture(category, name, confirm) +} diff --git a/compiler-scripts/src/test_runner/cli.rs b/compiler-scripts/src/test_runner/cli.rs index a3633391..fae93f76 100644 --- a/compiler-scripts/src/test_runner/cli.rs +++ b/compiler-scripts/src/test_runner/cli.rs @@ -21,6 +21,18 @@ pub struct SnapshotArgs { #[derive(Args, Clone, Debug)] pub struct RunArgs { + /// Create a new fixture directory with a template file + #[arg(long, value_name = "NAME")] + pub create: Option, + + /// Delete a fixture directory (dry-run unless --confirm) + #[arg(long, value_name = "NAME")] + pub delete: Option, + + /// Confirm deletion for --delete + #[arg(long)] + pub confirm: bool, + /// Subcommand (accept/reject) or test filters #[command(subcommand)] pub command: Option, diff --git a/compiler-scripts/src/test_runner/fixture.rs b/compiler-scripts/src/test_runner/fixture.rs new file mode 100644 index 00000000..0f5c43d6 --- /dev/null +++ b/compiler-scripts/src/test_runner/fixture.rs @@ -0,0 +1,213 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use console::style; +use heck::ToSnakeCase; + +use crate::test_runner::TestCategory; + +const MAIN_TEMPLATE: &str = "module Main where\n\n"; + +pub fn create_fixture(category: TestCategory, name: &str) -> Result { + let fixtures_dir = PathBuf::from(category.fixtures_subdir_fragment()); + if !fixtures_dir.is_dir() { + return Err(format!( + "fixtures directory '{}' does not exist", + fixtures_dir.display() + )); + } + + let next_number = next_fixture_number(&fixtures_dir)?; + let slug = slugify(name)?; + let folder_name = format!("{:03}_{}", next_number, slug); + let folder_path = fixtures_dir.join(&folder_name); + + if folder_path.exists() { + return Err(format!("fixture '{}' already exists", folder_path.display())); + } + + fs::create_dir_all(&folder_path).map_err(|err| { + format!( + "failed to create fixture directory '{}': {}", + folder_path.display(), + err + ) + })?; + + let main_path = folder_path.join("Main.purs"); + fs::write(&main_path, MAIN_TEMPLATE) + .map_err(|err| format!("failed to write '{}': {}", main_path.display(), err))?; + + println!( + "{} {}", + style("CREATED").green().bold(), + style(main_path.display()).cyan() + ); + println!(); + println!( + " {} {}", + style("Next:").dim(), + style(format!("just t {} {:03}", category.as_str(), next_number)).cyan() + ); + + Ok(folder_path) +} + +pub struct DeleteFixtureOutcome { + pub fixture_paths: Vec, + pub snapshot_paths: Vec, + pub confirmed: bool, +} + +pub fn delete_fixture( + category: TestCategory, + name: &str, + confirm: bool, +) -> Result { + let fixtures_dir = PathBuf::from(category.fixtures_subdir_fragment()); + if !fixtures_dir.is_dir() { + return Err(format!( + "fixtures directory '{}' does not exist", + fixtures_dir.display() + )); + } + + let fixture_paths = resolve_fixture_paths(&fixtures_dir, name)?; + let mut snapshot_paths = Vec::new(); + for fixture_path in &fixture_paths { + snapshot_paths.extend(find_snapshot_paths(category, fixture_path)?); + } + + if confirm { + for fixture_path in &fixture_paths { + if fixture_path.exists() { + fs::remove_dir_all(fixture_path).map_err(|err| { + format!( + "failed to delete fixture directory '{}': {}", + fixture_path.display(), + err + ) + })?; + } + } + + for snapshot_path in &snapshot_paths { + if snapshot_path.exists() { + fs::remove_file(snapshot_path).map_err(|err| { + format!( + "failed to delete snapshot '{}': {}", + snapshot_path.display(), + err + ) + })?; + } + } + } + + Ok(DeleteFixtureOutcome { fixture_paths, snapshot_paths, confirmed: confirm }) +} + +fn next_fixture_number(fixtures_dir: &Path) -> Result { + let mut max_number = 0; + let entries = fs::read_dir(fixtures_dir) + .map_err(|err| format!("failed to read '{}': {}", fixtures_dir.display(), err))?; + + for entry in entries { + let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + let Some((prefix, _)) = name.split_once('_') else { + continue; + }; + if prefix.len() != 3 || !prefix.chars().all(|ch| ch.is_ascii_digit()) { + continue; + } + if let Ok(number) = prefix.parse::() { + max_number = max_number.max(number); + } + } + + Ok(max_number + 1) +} + +fn resolve_fixture_paths(fixtures_dir: &Path, name: &str) -> Result, String> { + let slug = slugify(name)?; + let mut matches = find_matching_fixtures(fixtures_dir, &slug)?; + if matches.is_empty() && name.chars().all(|ch| ch.is_ascii_digit()) { + matches = find_matching_fixtures(fixtures_dir, name)?; + } + + if matches.is_empty() { + return Err(format!( + "no fixture found matching '{}' in '{}'", + name, + fixtures_dir.display() + )); + } + + matches.sort(); + Ok(matches) +} + +fn find_matching_fixtures(fixtures_dir: &Path, needle: &str) -> Result, String> { + let mut matches = Vec::new(); + let entries = fs::read_dir(fixtures_dir) + .map_err(|err| format!("failed to read '{}': {}", fixtures_dir.display(), err))?; + + for entry in entries { + let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + if !entry.path().is_dir() { + continue; + } + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.contains(needle) { + matches.push(entry.path()); + } + } + + Ok(matches) +} + +fn find_snapshot_paths( + category: TestCategory, + fixture_path: &Path, +) -> Result, String> { + let fixture_name = fixture_path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "fixture path is missing a valid folder name".to_string())?; + let fixture_slug = slugify(fixture_name).unwrap_or_else(|_| fixture_name.to_string()); + let mut paths = Vec::new(); + + for fragment in category.snapshot_path_fragments() { + let base = PathBuf::from(fragment); + if !base.exists() { + continue; + } + for entry in fs::read_dir(&base) + .map_err(|err| format!("failed to read '{}': {}", base.display(), err))? + { + let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + if !entry.path().is_file() { + continue; + } + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + if file_name.contains(&fixture_slug) { + paths.push(entry.path()); + } + } + } + + paths.sort(); + paths.dedup(); + Ok(paths) +} + +fn slugify(input: &str) -> Result { + if input.is_empty() { + return Err("fixture name must not be empty".to_string()); + } + Ok(input.to_snake_case()) +} diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs index 8f4b81d0..305cc661 100644 --- a/compiler-scripts/src/test_runner/nextest.rs +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -60,6 +60,9 @@ pub fn run_nextest( eprintln!("{}", style("Tests failed, re-running verbose...").yellow()); let verbose_args = RunArgs { + create: None, + delete: None, + confirm: false, command: None, filters: args.filters.clone(), verbose: true, diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index b2c28035..48952bab 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -26,7 +26,7 @@ tabled = "0.20.0" url = "2.5.7" [build-dependencies] -convert_case = "0.8.0" +heck = "0.5" itertools = "0.14.0" [dev-dependencies] diff --git a/tests-integration/build.rs b/tests-integration/build.rs index 036d6871..f76345e3 100644 --- a/tests-integration/build.rs +++ b/tests-integration/build.rs @@ -2,7 +2,7 @@ use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; -use convert_case::{Case, Converter}; +use heck::ToSnakeCase; use itertools::Itertools; fn read_dir<'output>(path: &Path) -> impl Iterator + use<'output> { @@ -44,10 +44,9 @@ fn run_test(folder: &str, file: &str) {{ ) .unwrap(); - let converter = Converter::new().to_case(Case::Snake); for folder in read_dir(Path::new("./fixtures/lsp")) { let Some(stem) = folder.file_stem() else { continue }; - let folder_name = converter.convert(stem.to_os_string().into_string().unwrap()); + let folder_name = stem.to_os_string().into_string().unwrap().to_snake_case(); writeln!( buffer, r#" @@ -73,14 +72,13 @@ fn run_test(folder: &str, file: &str) {{ settings.bind(|| insta::assert_snapshot!(file, report)); }}"#).unwrap(); - let converter = Converter::new().to_case(Case::Snake); for folder in read_dir(Path::new("./fixtures/lowering")) { let Some(stem) = folder.file_stem() else { continue }; - let folder_name = converter.convert(stem.to_os_string().into_string().unwrap()); + let folder_name = stem.to_os_string().into_string().unwrap().to_snake_case(); for file in read_purs_files(&folder) { let Some(file_stem) = file.file_stem() else { continue }; let file_name = file_stem.to_os_string().into_string().unwrap(); - let test_name = format!("{}_{}", folder_name, converter.convert(&file_name)); + let test_name = format!("{}_{}", folder_name, file_name.to_snake_case()); writeln!( buffer, r#" @@ -107,14 +105,13 @@ fn run_test(folder: &str, file: &str) {{ settings.bind(|| insta::assert_snapshot!(file, report)); }}"#).unwrap(); - let converter = Converter::new().to_case(Case::Snake); for folder in read_dir(Path::new("./fixtures/resolving")) { let Some(stem) = folder.file_stem() else { continue }; - let folder_name = converter.convert(stem.to_os_string().into_string().unwrap()); + let folder_name = stem.to_os_string().into_string().unwrap().to_snake_case(); for file in read_purs_files(&folder) { let Some(file_stem) = file.file_stem() else { continue }; let file_name = file_stem.to_os_string().into_string().unwrap(); - let test_name = format!("{}_{}", folder_name, converter.convert(&file_name)); + let test_name = format!("{}_{}", folder_name, file_name.to_snake_case()); writeln!( buffer, r#" @@ -157,10 +154,9 @@ fn run_test(folder: &str, file: &str) {{ settings.bind(|| insta::assert_snapshot!(file, report)); }}"#).unwrap(); - let converter = Converter::new().to_case(Case::Snake); for folder in read_dir(Path::new("./fixtures/checking")) { let Some(stem) = folder.file_stem() else { continue }; - let folder_name = converter.convert(stem.to_os_string().into_string().unwrap()); + let folder_name = stem.to_os_string().into_string().unwrap().to_snake_case(); // Skip the prelude folder - it's shared setup, not a test if folder_name == "prelude" { continue; From 67b782fce7616ba468a07ed38b684da1d1e4d3a4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 13:13:29 +0800 Subject: [PATCH 102/386] Update compiler-scripts to use anyhow --- Cargo.lock | 7 ++ compiler-scripts/Cargo.toml | 1 + compiler-scripts/src/main.rs | 30 +++++-- compiler-scripts/src/test_runner.rs | 38 +++++---- compiler-scripts/src/test_runner/category.rs | 8 +- compiler-scripts/src/test_runner/fixture.rs | 83 +++++++------------- compiler-scripts/src/test_runner/nextest.rs | 11 +-- compiler-scripts/src/test_runner/pending.rs | 18 +++-- 8 files changed, 105 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e7cdb7d..70791449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arrayvec" version = "0.5.2" @@ -413,6 +419,7 @@ dependencies = [ name = "compiler-scripts" version = "0.1.0" dependencies = [ + "anyhow", "clap", "console", "heck", diff --git a/compiler-scripts/Cargo.toml b/compiler-scripts/Cargo.toml index bb2f96d5..f4703f35 100644 --- a/compiler-scripts/Cargo.toml +++ b/compiler-scripts/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.100" clap = { version = "4", features = ["derive"] } console = "0.15" heck = "0.5" diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs index 10733d8f..89d36188 100644 --- a/compiler-scripts/src/main.rs +++ b/compiler-scripts/src/main.rs @@ -21,7 +21,7 @@ fn main() { if let Some(name) = &cli.args.create { if let Err(error) = create_fixture(cli.category, name) { - eprintln!("{}", error); + eprintln!("{:#}", error); std::process::exit(1); } return; @@ -71,7 +71,7 @@ fn main() { } } Err(error) => { - eprintln!("{}", error); + eprintln!("{:#}", error); std::process::exit(1); } } @@ -80,22 +80,40 @@ fn main() { match &cli.args.command { Some(CategoryCommand::Accept(args)) => { - let outcome = accept_category(cli.category, args); + let outcome = match accept_category(cli.category, args) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); + std::process::exit(1); + } + }; if !outcome.success { std::process::exit(1); } } Some(CategoryCommand::Reject(args)) => { - let outcome = reject_category(cli.category, args); + let outcome = match reject_category(cli.category, args) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); + std::process::exit(1); + } + }; if !outcome.success { std::process::exit(1); } } None => { - let outcome = run_category(cli.category, &cli.args); + let outcome = match run_category(cli.category, &cli.args) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); + std::process::exit(1); + } + }; if !outcome.tests_passed { std::process::exit(1); } } } -} \ No newline at end of file +} diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs index dc3af4ce..1e983413 100644 --- a/compiler-scripts/src/test_runner.rs +++ b/compiler-scripts/src/test_runner.rs @@ -23,20 +23,20 @@ pub struct TestOutcome { pub trace_paths: Vec, } -pub fn run_category(category: TestCategory, args: &RunArgs) -> TestOutcome { +pub fn run_category(category: TestCategory, args: &RunArgs) -> anyhow::Result { // 1. Hash fixtures and print timing let start = Instant::now(); let fixture_hashes = crate::fixtures::fixture_env(); println!("{}", style(format!("Hashed fixtures in {}ms", start.elapsed().as_millis())).dim()); // 2. Run nextest - let tests_passed = nextest::run_nextest(category, args, &fixture_hashes); + let tests_passed = nextest::run_nextest(category, args, &fixture_hashes)?; // 3. Collect trace paths let trace_paths = traces::collect_trace_paths(&args.filters, args.debug); // 4. Process pending snapshots - let pending_result = pending::process_pending_snapshots(category, args, &trace_paths); + let pending_result = pending::process_pending_snapshots(category, args, &trace_paths)?; // 5. Print next actions ui::print_next_actions(NextActionsArgs { @@ -51,7 +51,7 @@ pub fn run_category(category: TestCategory, args: &RunArgs) -> TestOutcome { showed_diffs: args.diff, }); - TestOutcome { tests_passed, pending_count: pending_result.count, trace_paths } + Ok(TestOutcome { tests_passed, pending_count: pending_result.count, trace_paths }) } pub struct SnapshotOutcome { @@ -59,12 +59,15 @@ pub struct SnapshotOutcome { pub count: usize, } -pub fn accept_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotOutcome { - let snapshots = pending::collect_pending_snapshots(category, &args.filters); +pub fn accept_category( + category: TestCategory, + args: &SnapshotArgs, +) -> anyhow::Result { + let snapshots = pending::collect_pending_snapshots(category, &args.filters)?; if snapshots.is_empty() { println!("{}", style("No pending snapshots found.").dim()); - return SnapshotOutcome { success: true, count: 0 }; + return Ok(SnapshotOutcome { success: true, count: 0 }); } if !args.all && args.filters.is_empty() { @@ -78,22 +81,25 @@ pub fn accept_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotO "To accept all, run: {}", style(format!("just t {} accept --all", category.as_str())).cyan() ); - return SnapshotOutcome { success: false, count: 0 }; + return Ok(SnapshotOutcome { success: false, count: 0 }); } let result = pending::accept_snapshots(&snapshots); println!(); println!("{}", style(format!("Accepted {} snapshot(s)", result.accepted)).green()); - SnapshotOutcome { success: result.failed == 0, count: result.accepted } + Ok(SnapshotOutcome { success: result.failed == 0, count: result.accepted }) } -pub fn reject_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotOutcome { - let snapshots = pending::collect_pending_snapshots(category, &args.filters); +pub fn reject_category( + category: TestCategory, + args: &SnapshotArgs, +) -> anyhow::Result { + let snapshots = pending::collect_pending_snapshots(category, &args.filters)?; if snapshots.is_empty() { println!("{}", style("No pending snapshots found.").dim()); - return SnapshotOutcome { success: true, count: 0 }; + return Ok(SnapshotOutcome { success: true, count: 0 }); } if !args.all && args.filters.is_empty() { @@ -107,19 +113,19 @@ pub fn reject_category(category: TestCategory, args: &SnapshotArgs) -> SnapshotO "To reject all, run: {}", style(format!("just t {} reject --all", category.as_str())).cyan() ); - return SnapshotOutcome { success: false, count: 0 }; + return Ok(SnapshotOutcome { success: false, count: 0 }); } let result = pending::reject_snapshots(&snapshots); println!(); println!("{}", style(format!("Rejected {} snapshot(s)", result.rejected)).red()); - SnapshotOutcome { success: result.failed == 0, count: result.rejected } + Ok(SnapshotOutcome { success: result.failed == 0, count: result.rejected }) } pub use fixture::DeleteFixtureOutcome; -pub fn create_fixture(category: TestCategory, name: &str) -> Result<(), String> { +pub fn create_fixture(category: TestCategory, name: &str) -> anyhow::Result<()> { fixture::create_fixture(category, name).map(|_| ()) } @@ -127,6 +133,6 @@ pub fn delete_fixture( category: TestCategory, name: &str, confirm: bool, -) -> Result { +) -> anyhow::Result { fixture::delete_fixture(category, name, confirm) } diff --git a/compiler-scripts/src/test_runner/category.rs b/compiler-scripts/src/test_runner/category.rs index c26cc949..932770e2 100644 --- a/compiler-scripts/src/test_runner/category.rs +++ b/compiler-scripts/src/test_runner/category.rs @@ -1,6 +1,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; +use anyhow::bail; + #[derive(Copy, Clone, Debug)] pub enum TestCategory { Checking, @@ -45,7 +47,7 @@ impl TestCategory { } impl FromStr for TestCategory { - type Err = String; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { @@ -53,10 +55,10 @@ impl FromStr for TestCategory { "lowering" | "l" => Ok(TestCategory::Lowering), "resolving" | "r" => Ok(TestCategory::Resolving), "lsp" => Ok(TestCategory::Lsp), - _ => Err(format!( + _ => bail!( "unknown test category '{}', expected: checking (c), lowering (l), resolving (r), lsp", s - )), + ), } } } diff --git a/compiler-scripts/src/test_runner/fixture.rs b/compiler-scripts/src/test_runner/fixture.rs index 0f5c43d6..4f79772f 100644 --- a/compiler-scripts/src/test_runner/fixture.rs +++ b/compiler-scripts/src/test_runner/fixture.rs @@ -1,6 +1,7 @@ use std::fs; use std::path::{Path, PathBuf}; +use anyhow::{Context, bail}; use console::style; use heck::ToSnakeCase; @@ -8,13 +9,10 @@ use crate::test_runner::TestCategory; const MAIN_TEMPLATE: &str = "module Main where\n\n"; -pub fn create_fixture(category: TestCategory, name: &str) -> Result { +pub fn create_fixture(category: TestCategory, name: &str) -> anyhow::Result { let fixtures_dir = PathBuf::from(category.fixtures_subdir_fragment()); if !fixtures_dir.is_dir() { - return Err(format!( - "fixtures directory '{}' does not exist", - fixtures_dir.display() - )); + bail!("fixtures directory '{}' does not exist", fixtures_dir.display()); } let next_number = next_fixture_number(&fixtures_dir)?; @@ -23,26 +21,18 @@ pub fn create_fixture(category: TestCategory, name: &str) -> Result Result { +) -> anyhow::Result { let fixtures_dir = PathBuf::from(category.fixtures_subdir_fragment()); if !fixtures_dir.is_dir() { - return Err(format!( - "fixtures directory '{}' does not exist", - fixtures_dir.display() - )); + bail!("fixtures directory '{}' does not exist", fixtures_dir.display()); } let fixture_paths = resolve_fixture_paths(&fixtures_dir, name)?; @@ -81,24 +68,16 @@ pub fn delete_fixture( if confirm { for fixture_path in &fixture_paths { if fixture_path.exists() { - fs::remove_dir_all(fixture_path).map_err(|err| { - format!( - "failed to delete fixture directory '{}': {}", - fixture_path.display(), - err - ) + fs::remove_dir_all(fixture_path).with_context(|| { + format!("failed to delete fixture directory '{}'", fixture_path.display()) })?; } } for snapshot_path in &snapshot_paths { if snapshot_path.exists() { - fs::remove_file(snapshot_path).map_err(|err| { - format!( - "failed to delete snapshot '{}': {}", - snapshot_path.display(), - err - ) + fs::remove_file(snapshot_path).with_context(|| { + format!("failed to delete snapshot '{}'", snapshot_path.display()) })?; } } @@ -107,13 +86,13 @@ pub fn delete_fixture( Ok(DeleteFixtureOutcome { fixture_paths, snapshot_paths, confirmed: confirm }) } -fn next_fixture_number(fixtures_dir: &Path) -> Result { +fn next_fixture_number(fixtures_dir: &Path) -> anyhow::Result { let mut max_number = 0; let entries = fs::read_dir(fixtures_dir) - .map_err(|err| format!("failed to read '{}': {}", fixtures_dir.display(), err))?; + .with_context(|| format!("failed to read '{}'", fixtures_dir.display()))?; for entry in entries { - let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + let entry = entry.context("failed to read entry")?; let name = entry.file_name(); let name = name.to_string_lossy(); let Some((prefix, _)) = name.split_once('_') else { @@ -130,7 +109,7 @@ fn next_fixture_number(fixtures_dir: &Path) -> Result { Ok(max_number + 1) } -fn resolve_fixture_paths(fixtures_dir: &Path, name: &str) -> Result, String> { +fn resolve_fixture_paths(fixtures_dir: &Path, name: &str) -> anyhow::Result> { let slug = slugify(name)?; let mut matches = find_matching_fixtures(fixtures_dir, &slug)?; if matches.is_empty() && name.chars().all(|ch| ch.is_ascii_digit()) { @@ -138,24 +117,20 @@ fn resolve_fixture_paths(fixtures_dir: &Path, name: &str) -> Result } if matches.is_empty() { - return Err(format!( - "no fixture found matching '{}' in '{}'", - name, - fixtures_dir.display() - )); + bail!("no fixture found matching '{}' in '{}'", name, fixtures_dir.display()); } matches.sort(); Ok(matches) } -fn find_matching_fixtures(fixtures_dir: &Path, needle: &str) -> Result, String> { +fn find_matching_fixtures(fixtures_dir: &Path, needle: &str) -> anyhow::Result> { let mut matches = Vec::new(); let entries = fs::read_dir(fixtures_dir) - .map_err(|err| format!("failed to read '{}': {}", fixtures_dir.display(), err))?; + .with_context(|| format!("failed to read '{}'", fixtures_dir.display()))?; for entry in entries { - let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + let entry = entry.context("failed to read entry")?; if !entry.path().is_dir() { continue; } @@ -172,11 +147,11 @@ fn find_matching_fixtures(fixtures_dir: &Path, needle: &str) -> Result Result, String> { +) -> anyhow::Result> { let fixture_name = fixture_path .file_name() .and_then(|name| name.to_str()) - .ok_or_else(|| "fixture path is missing a valid folder name".to_string())?; + .context("fixture path is missing a valid folder name")?; let fixture_slug = slugify(fixture_name).unwrap_or_else(|_| fixture_name.to_string()); let mut paths = Vec::new(); @@ -185,10 +160,10 @@ fn find_snapshot_paths( if !base.exists() { continue; } - for entry in fs::read_dir(&base) - .map_err(|err| format!("failed to read '{}': {}", base.display(), err))? + for entry in + fs::read_dir(&base).with_context(|| format!("failed to read '{}'", base.display()))? { - let entry = entry.map_err(|err| format!("failed to read entry: {}", err))?; + let entry = entry.context("failed to read entry")?; if !entry.path().is_file() { continue; } @@ -205,9 +180,9 @@ fn find_snapshot_paths( Ok(paths) } -fn slugify(input: &str) -> Result { +fn slugify(input: &str) -> anyhow::Result { if input.is_empty() { - return Err("fixture name must not be empty".to_string()); + bail!("fixture name must not be empty"); } Ok(input.to_snake_case()) } diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs index 305cc661..94db0428 100644 --- a/compiler-scripts/src/test_runner/nextest.rs +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::process::{Command, Stdio}; +use anyhow::Context; use console::style; use crate::test_runner::category::TestCategory; @@ -46,15 +47,15 @@ pub fn run_nextest( category: TestCategory, args: &RunArgs, fixture_hashes: &HashMap, -) -> bool { +) -> anyhow::Result { let mut cmd = build_nextest_command(category, args, fixture_hashes); if args.verbose { - let status = cmd.status().expect("Failed to run cargo nextest"); - status.success() + let status = cmd.status().context("failed to run cargo nextest")?; + Ok(status.success()) } else { cmd.stdout(Stdio::null()).stderr(Stdio::null()); - let status = cmd.status().expect("Failed to run cargo nextest"); + let status = cmd.status().context("failed to run cargo nextest")?; if !status.success() { eprintln!("{}", style("Tests failed, re-running verbose...").yellow()); @@ -75,6 +76,6 @@ pub fn run_nextest( let _ = retry.status(); } - status.success() + Ok(status.success()) } } diff --git a/compiler-scripts/src/test_runner/pending.rs b/compiler-scripts/src/test_runner/pending.rs index abb55867..9de12178 100644 --- a/compiler-scripts/src/test_runner/pending.rs +++ b/compiler-scripts/src/test_runner/pending.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::{env, fs}; +use anyhow::Context; use console::style; use serde::Deserialize; @@ -37,19 +38,22 @@ impl SnapshotInfo { } /// Collect pending snapshots for a category, optionally filtered. -pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> Vec { +pub fn collect_pending_snapshots( + category: TestCategory, + filters: &[String], +) -> anyhow::Result> { let pending_output = Command::new("cargo") .arg("insta") .arg("pending-snapshots") .arg("--as-json") .stderr(Stdio::null()) .output() - .expect("Failed to run cargo insta"); + .context("failed to run cargo insta")?; let pending = String::from_utf8_lossy(&pending_output.stdout); let pending = pending.trim(); - let cwd = env::current_dir().unwrap(); + let cwd = env::current_dir().context("failed to get working directory")?; let path_fragments = category.snapshot_path_fragments(); let mut snapshots = Vec::new(); @@ -93,7 +97,7 @@ pub fn collect_pending_snapshots(category: TestCategory, filters: &[String]) -> }); } - snapshots + Ok(snapshots) } fn collect_exclusion_patterns(args: &RunArgs) -> Vec { @@ -130,8 +134,8 @@ pub fn process_pending_snapshots( category: TestCategory, args: &RunArgs, trace_paths: &[PathBuf], -) -> PendingResult { - let mut snapshots = collect_pending_snapshots(category, &args.filters); +) -> anyhow::Result { + let mut snapshots = collect_pending_snapshots(category, &args.filters)?; // Populate trace paths for info in &mut snapshots { @@ -194,7 +198,7 @@ pub fn process_pending_snapshots( ); } - PendingResult { count: visible.len(), excluded_count, total_lines_changed } + Ok(PendingResult { count: visible.len(), excluded_count, total_lines_changed }) } pub struct AcceptRejectResult { From 8964fe04ac78024079762f25fbff011eddadc41c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 18:40:46 +0800 Subject: [PATCH 103/386] Fix argument passing in justfile --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index bd3bf080..a84cbd71 100644 --- a/justfile +++ b/justfile @@ -26,11 +26,11 @@ coverage-html: [doc("Run integration tests with snapshot diffing: checking|lowering|resolving|lsp")] @t *args="": - cargo run -q -p compiler-scripts -- {{args}} + cargo run -q -p compiler-scripts -- "$@" [doc("Shorthand for 'just t checking'")] @tc *args="": - cargo run -q -p compiler-scripts -- checking {{args}} + cargo run -q -p compiler-scripts -- checking "$@" [doc("Apply clippy fixes and format")] fix: From e8ff7c091015db6fe2f44fc26a229b73f507ebd6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 20:44:27 +0800 Subject: [PATCH 104/386] Initial support for compiler-generated Partial This commit also fixes an issue in infer_case_of where the subtype call is reversed, which led to skolemisation of the expression's type. The infer_case_of function is now aligned with infer_equations_core. This commit also adds instantiation for the result_type at the end of infer_sectioned_expression. This mirrors infer_lambda where it also instantiates the body_type to remove constraints. --- .../checking/src/algorithm/quantify.rs | 13 ++++++- compiler-core/checking/src/algorithm/term.rs | 15 ++++++-- .../checking/255_exhaustive_basic/Main.snap | 4 +-- .../256_exhaustive_multiple/Main.snap | 12 ++++--- .../checking/257_exhaustive_tuple/Main.snap | 12 ++++--- .../259_exhaustive_boolean_partial/Main.snap | 15 ++++++++ .../260_exhaustive_integer_partial/Main.snap | 10 ++++++ .../261_exhaustive_number_partial/Main.snap | 10 ++++++ .../262_exhaustive_char_partial/Main.snap | 10 ++++++ .../263_exhaustive_string_partial/Main.snap | 10 ++++++ .../278_partial_case_nested/Main.purs | 17 ++++++++++ .../278_partial_case_nested/Main.snap | 34 +++++++++++++++++++ .../checking/279_partial_let_where/Main.purs | 27 +++++++++++++++ .../checking/279_partial_let_where/Main.snap | 34 +++++++++++++++++++ .../280_partial_case_variable/Main.purs | 9 +++++ .../280_partial_case_variable/Main.snap | 34 +++++++++++++++++++ .../Main.purs | 9 +++++ .../Main.snap | 25 ++++++++++++++ .../checking/prelude/Partial.Unsafe.purs | 6 ++++ tests-integration/tests/checking/generated.rs | 8 +++++ 20 files changed, 300 insertions(+), 14 deletions(-) create mode 100644 tests-integration/fixtures/checking/278_partial_case_nested/Main.purs create mode 100644 tests-integration/fixtures/checking/278_partial_case_nested/Main.snap create mode 100644 tests-integration/fixtures/checking/279_partial_let_where/Main.purs create mode 100644 tests-integration/fixtures/checking/279_partial_let_where/Main.snap create mode 100644 tests-integration/fixtures/checking/280_partial_case_variable/Main.purs create mode 100644 tests-integration/fixtures/checking/280_partial_case_variable/Main.snap create mode 100644 tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.purs create mode 100644 tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap create mode 100644 tests-integration/fixtures/checking/prelude/Partial.Unsafe.purs diff --git a/compiler-core/checking/src/algorithm/quantify.rs b/compiler-core/checking/src/algorithm/quantify.rs index b64ba1bf..dad258aa 100644 --- a/compiler-core/checking/src/algorithm/quantify.rs +++ b/compiler-core/checking/src/algorithm/quantify.rs @@ -98,9 +98,20 @@ where let mut pending = vec![]; let mut unsatisfied = vec![]; + let mut latent = vec![]; for constraint in constraints { let constraint = Zonk::on(state, constraint); + + // Partial is a latent constraint, it has no type arguments so it + // never has unification variables, but should be generalised + // rather than reported as unsatisfied. This allows inferring + // `Partial => Int` for expressions with non-exhaustive patterns. + if constraint == context.prim.partial { + latent.push(constraint); + continue; + } + let unification: FxHashSet = collect_unification(state, constraint).nodes().collect(); if unification.is_empty() { unsatisfied.push(constraint); @@ -113,7 +124,7 @@ where let (generalised, ambiguous) = classify_constraints_by_reachability(pending, in_signature); // Subtle: stable ordering for consistent output - let generalised = generalised.into_iter().sorted().collect_vec(); + let generalised = latent.into_iter().chain(generalised).sorted().collect_vec(); let minimized = minimize_by_superclasses(state, context, generalised)?; let constrained_type = minimized.into_iter().rfold(type_id, |constrained, constraint| { diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 486b42fe..62639632 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -110,7 +110,9 @@ where }); let parameter_types = parameter_types.collect_vec(); + let result_type = infer_expression_core(state, context, expr_id)?; + let result_type = toolkit::instantiate_constrained(state, result_type); Ok(state.make_function(¶meter_types, result_type)) } @@ -352,7 +354,7 @@ fn infer_case_of( where Q: ExternalQueries, { - let inferred_type = state.fresh_unification_type(context); + let result_type = state.fresh_unification_type(context); let mut trunk_types = vec![]; for trunk in trunk.iter() { @@ -366,15 +368,22 @@ where } if let Some(guarded) = &branch.guarded_expression { let guarded_type = infer_guarded_expression(state, context, guarded)?; - let _ = unification::subtype(state, context, inferred_type, guarded_type)?; + let _ = unification::subtype(state, context, guarded_type, result_type)?; } } let exhaustiveness = exhaustiveness::check_case_patterns(state, context, &trunk_types, branches)?; + + let has_missing = exhaustiveness.missing.is_some(); state.report_exhaustiveness(exhaustiveness); - Ok(inferred_type) + if has_missing { + let constrained_type = Type::Constrained(context.prim.partial, result_type); + Ok(state.storage.intern(constrained_type)) + } else { + Ok(result_type) + } } /// Lookup `bind` from resolution, or synthesize `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. diff --git a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap index 6950447c..6f813729 100644 --- a/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap +++ b/tests-integration/fixtures/checking/255_exhaustive_basic/Main.snap @@ -6,8 +6,8 @@ expression: report Terms Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) Nothing :: forall (a :: Type). Maybe (a :: Type) -test1 :: forall (t5 :: Type). Maybe (t5 :: Type) -> Int -test2 :: forall (t10 :: Type). Maybe (t10 :: Type) -> Int +test1 :: forall (t5 :: Type). Partial => Maybe (t5 :: Type) -> Int +test2 :: forall (t10 :: Type). Partial => Maybe (t10 :: Type) -> Int Types Maybe :: Type -> Type diff --git a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap index d781101d..b6c48487 100644 --- a/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap +++ b/tests-integration/fixtures/checking/256_exhaustive_multiple/Main.snap @@ -7,10 +7,14 @@ Terms Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) Nothing :: forall (a :: Type). Maybe (a :: Type) complete :: forall (t6 :: Type) (t7 :: Type). Maybe (t6 :: Type) -> Maybe (t7 :: Type) -> Int -incomplete1 :: forall (t19 :: Type) (t20 :: Type). Maybe (t19 :: Type) -> Maybe (t20 :: Type) -> Int -incomplete2 :: forall (t30 :: Type) (t31 :: Type). Maybe (t30 :: Type) -> Maybe (t31 :: Type) -> Int -incomplete3 :: forall (t41 :: Type) (t42 :: Type). Maybe (t41 :: Type) -> Maybe (t42 :: Type) -> Int -incomplete4 :: forall (t52 :: Type) (t53 :: Type). Maybe (t52 :: Type) -> Maybe (t53 :: Type) -> Int +incomplete1 :: + forall (t19 :: Type) (t20 :: Type). Partial => Maybe (t19 :: Type) -> Maybe (t20 :: Type) -> Int +incomplete2 :: + forall (t30 :: Type) (t31 :: Type). Partial => Maybe (t30 :: Type) -> Maybe (t31 :: Type) -> Int +incomplete3 :: + forall (t41 :: Type) (t42 :: Type). Partial => Maybe (t41 :: Type) -> Maybe (t42 :: Type) -> Int +incomplete4 :: + forall (t52 :: Type) (t53 :: Type). Partial => Maybe (t52 :: Type) -> Maybe (t53 :: Type) -> Int Types Maybe :: Type -> Type diff --git a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap index 8e78ae6a..4f91ed1e 100644 --- a/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap +++ b/tests-integration/fixtures/checking/257_exhaustive_tuple/Main.snap @@ -10,13 +10,17 @@ Nothing :: forall (a :: Type). Maybe (a :: Type) complete :: forall (t9 :: Type) (t10 :: Type). Tuple (Maybe (t9 :: Type)) (Maybe (t10 :: Type)) -> Int incomplete1 :: - forall (t29 :: Type) (t30 :: Type). Tuple (Maybe (t29 :: Type)) (Maybe (t30 :: Type)) -> Int + forall (t29 :: Type) (t30 :: Type). + Partial => Tuple (Maybe (t29 :: Type)) (Maybe (t30 :: Type)) -> Int incomplete2 :: - forall (t45 :: Type) (t46 :: Type). Tuple (Maybe (t45 :: Type)) (Maybe (t46 :: Type)) -> Int + forall (t45 :: Type) (t46 :: Type). + Partial => Tuple (Maybe (t45 :: Type)) (Maybe (t46 :: Type)) -> Int incomplete3 :: - forall (t61 :: Type) (t62 :: Type). Tuple (Maybe (t61 :: Type)) (Maybe (t62 :: Type)) -> Int + forall (t61 :: Type) (t62 :: Type). + Partial => Tuple (Maybe (t61 :: Type)) (Maybe (t62 :: Type)) -> Int incomplete4 :: - forall (t77 :: Type) (t78 :: Type). Tuple (Maybe (t77 :: Type)) (Maybe (t78 :: Type)) -> Int + forall (t77 :: Type) (t78 :: Type). + Partial => Tuple (Maybe (t77 :: Type)) (Maybe (t78 :: Type)) -> Int Types Tuple :: Type -> Type -> Type diff --git a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap index d147a022..0a1e1088 100644 --- a/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap +++ b/tests-integration/fixtures/checking/259_exhaustive_boolean_partial/Main.snap @@ -18,13 +18,28 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: false | 4 | testPartialTrue b = case b of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 3:1..3:34 + | +3 | testPartialTrue :: Boolean -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: true --> 8:22..9:13 | 8 | testPartialFalse b = case b of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 7:1..7:35 + | +7 | testPartialFalse :: Boolean -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: false --> 18:11..19:14 | 18 | true -> case y of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 16:1..16:47 + | +16 | testNestedPartial :: Boolean -> Boolean -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap index 65c7f66a..9b97290a 100644 --- a/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap +++ b/tests-integration/fixtures/checking/260_exhaustive_integer_partial/Main.snap @@ -16,8 +16,18 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ | 4 | testPartialZero n = case n of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 3:1..3:30 + | +3 | testPartialZero :: Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:20..9:9 | 8 | testPartialOne n = case n of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 7:1..7:29 + | +7 | testPartialOne :: Int -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap index 81121e46..0db42737 100644 --- a/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap +++ b/tests-integration/fixtures/checking/261_exhaustive_number_partial/Main.snap @@ -16,8 +16,18 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ | 4 | testPartialZero n = case n of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 3:1..3:33 + | +3 | testPartialZero :: Number -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:24..9:11 | 8 | testPartialOneFive n = case n of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 7:1..7:36 + | +7 | testPartialOneFive :: Number -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap index 27767cbb..cca1ff1b 100644 --- a/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap +++ b/tests-integration/fixtures/checking/262_exhaustive_char_partial/Main.snap @@ -16,8 +16,18 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ | 4 | testPartialA c = case c of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 3:1..3:28 + | +3 | testPartialA :: Char -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:18..9:11 | 8 | testPartialB c = case c of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 7:1..7:28 + | +7 | testPartialB :: Char -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap index d36983fa..8cf49631 100644 --- a/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap +++ b/tests-integration/fixtures/checking/263_exhaustive_string_partial/Main.snap @@ -16,8 +16,18 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ | 4 | testPartialHello s = case s of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 3:1..3:34 + | +3 | testPartialHello :: String -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ --> 8:22..9:15 | 8 | testPartialWorld s = case s of | ^~~~~~~~~ +error[NoInstanceFound]: No instance found for: Partial + --> 7:1..7:34 + | +7 | testPartialWorld :: String -> Int + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/278_partial_case_nested/Main.purs b/tests-integration/fixtures/checking/278_partial_case_nested/Main.purs new file mode 100644 index 00000000..cbf23517 --- /dev/null +++ b/tests-integration/fixtures/checking/278_partial_case_nested/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Partial.Unsafe (unsafePartial) + +partialCase :: Partial => Int +partialCase = case 123 of + 123 -> 123 + +partialCase' = case 123 of + 123 -> 123 + +partialNested :: Int +partialNested = unsafePartial (case 123 of + 123 -> 123) + +partialNested' = unsafePartial (case 123 of + 123 -> 123) diff --git a/tests-integration/fixtures/checking/278_partial_case_nested/Main.snap b/tests-integration/fixtures/checking/278_partial_case_nested/Main.snap new file mode 100644 index 00000000..9dd3e74d --- /dev/null +++ b/tests-integration/fixtures/checking/278_partial_case_nested/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +partialCase :: Partial => Int +partialCase' :: Partial => Int +partialNested :: Int +partialNested' :: Int + +Types + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 6:15..7:13 + | +6 | partialCase = case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 9:16..10:13 + | +9 | partialCase' = case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 13:32..14:13 + | +13 | partialNested = unsafePartial (case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 16:33..17:13 + | +16 | partialNested' = unsafePartial (case 123 of + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/279_partial_let_where/Main.purs b/tests-integration/fixtures/checking/279_partial_let_where/Main.purs new file mode 100644 index 00000000..e6383cc0 --- /dev/null +++ b/tests-integration/fixtures/checking/279_partial_let_where/Main.purs @@ -0,0 +1,27 @@ +module Main where + +partialLet :: Partial => Int +partialLet = + let + value = case 123 of + 123 -> 123 + in + value + +partialLet' = + let + value = case 123 of + 123 -> 123 + in + value + +partialWhere :: Partial => Int +partialWhere = value + where + value = case 123 of + 123 -> 123 + +partialWhere' = value + where + value = case 123 of + 123 -> 123 diff --git a/tests-integration/fixtures/checking/279_partial_let_where/Main.snap b/tests-integration/fixtures/checking/279_partial_let_where/Main.snap new file mode 100644 index 00000000..190dcc64 --- /dev/null +++ b/tests-integration/fixtures/checking/279_partial_let_where/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +partialLet :: Partial => Int +partialLet' :: Partial => Int +partialWhere :: Partial => Int +partialWhere' :: Partial => Int + +Types + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 6:13..7:17 + | +6 | value = case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 13:13..14:17 + | +13 | value = case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 21:11..22:15 + | +21 | value = case 123 of + | ^~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ + --> 26:11..27:15 + | +26 | value = case 123 of + | ^~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/280_partial_case_variable/Main.purs b/tests-integration/fixtures/checking/280_partial_case_variable/Main.purs new file mode 100644 index 00000000..8332b7fe --- /dev/null +++ b/tests-integration/fixtures/checking/280_partial_case_variable/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Maybe a = Just a | Nothing + +test a = case a of + Just 123 -> 123 + +test' = case _ of + Just 123 -> 123 diff --git a/tests-integration/fixtures/checking/280_partial_case_variable/Main.snap b/tests-integration/fixtures/checking/280_partial_case_variable/Main.snap new file mode 100644 index 00000000..90196e14 --- /dev/null +++ b/tests-integration/fixtures/checking/280_partial_case_variable/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Partial => Maybe Int -> Int +test' :: Partial => Maybe Int -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 5:10..6:18 + | +5 | test a = case a of + | ^~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 8:9..9:18 + | +8 | test' = case _ of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.purs b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.purs new file mode 100644 index 00000000..9a0aae6f --- /dev/null +++ b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Unit = Unit + +class Example a where + example :: a + +test = case _ of + Unit -> example diff --git a/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap new file mode 100644 index 00000000..dc23e4cc --- /dev/null +++ b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +example :: forall (a :: Type). Example (a :: Type) => (a :: Type) +test :: forall (t4 :: Type). Example (t4 :: Type) => Unit -> (t4 :: Type) + +Types +Unit :: Type +Example :: Type -> Constraint + +Data +Unit + Quantified = :0 + Kind = :0 + + +Roles +Unit = [] + +Classes +class Example (&0 :: Type) diff --git a/tests-integration/fixtures/checking/prelude/Partial.Unsafe.purs b/tests-integration/fixtures/checking/prelude/Partial.Unsafe.purs new file mode 100644 index 00000000..b92decc3 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Partial.Unsafe.purs @@ -0,0 +1,6 @@ +module Partial.Unsafe where + +import Safe.Coerce (unsafeCoerce) + +unsafePartial :: forall a. (Partial => a) -> a +unsafePartial = unsafeCoerce diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a131d973..67356299 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -569,3 +569,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_276_where_clause_outer_scope_main() { run_test("276_where_clause_outer_scope", "Main"); } #[rustfmt::skip] #[test] fn test_277_keyword_as_variable_main() { run_test("277_keyword_as_variable", "Main"); } + +#[rustfmt::skip] #[test] fn test_278_partial_case_nested_main() { run_test("278_partial_case_nested", "Main"); } + +#[rustfmt::skip] #[test] fn test_279_partial_let_where_main() { run_test("279_partial_let_where", "Main"); } + +#[rustfmt::skip] #[test] fn test_280_partial_case_variable_main() { run_test("280_partial_case_variable", "Main"); } + +#[rustfmt::skip] #[test] fn test_281_sectioned_constraint_generation_main() { run_test("281_sectioned_constraint_generation", "Main"); } From e933a882187fc1619252701d4f45442c326ab135 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Feb 2026 23:09:45 +0800 Subject: [PATCH 105/386] Fix promotion rules for higher-rank unification --- .../checking/src/algorithm/unification.rs | 197 ++++++++++-------- .../282_higher_rank_unification/Lib.purs | 12 ++ .../282_higher_rank_unification/Main.purs | 16 ++ .../282_higher_rank_unification/Main.snap | 23 ++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 168 insertions(+), 82 deletions(-) create mode 100644 tests-integration/fixtures/checking/282_higher_rank_unification/Lib.purs create mode 100644 tests-integration/fixtures/checking/282_higher_rank_unification/Main.purs create mode 100644 tests-integration/fixtures/checking/282_higher_rank_unification/Main.snap diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 474afee5..16f910b5 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -371,121 +371,154 @@ where pub fn promote_type( state: &mut CheckState, - codomain: debruijn::Size, + initial_codomain: debruijn::Size, unification_id: u32, solution: TypeId, ) -> bool { - let solution = state.normalize_type(solution); - match state.storage[solution] { - Type::Application(function, argument) => { - promote_type(state, codomain, unification_id, function) - && promote_type(state, codomain, unification_id, argument) - } + /// Invariant context for the inner recursion of [`promote_type`]. + struct PromoteContext { + /// The type scope size when calling [`solve`]. + /// + /// Bound variables at or above this level are introduced + /// by foralls within the solution and don't escape. + initial_codomain: debruijn::Size, + /// The unification variable being solved. + unification_id: u32, + } - Type::Constrained(constraint, inner) => { - promote_type(state, codomain, unification_id, constraint) - && promote_type(state, codomain, unification_id, inner) - } + fn aux(s: &mut CheckState, c: &PromoteContext, codomain: debruijn::Size, t: TypeId) -> bool { + let t = s.normalize_type(t); + match s.storage[t] { + Type::Application(function, argument) => { + aux(s, c, codomain, function) && aux(s, c, codomain, argument) + } - Type::Constructor(_, _) => true, + Type::Constrained(constraint, inner) => { + aux(s, c, codomain, constraint) && aux(s, c, codomain, inner) + } - Type::Forall(ref binder, inner) => { - let inner_codomain = codomain.increment(); - promote_type(state, codomain, unification_id, binder.kind) - && promote_type(state, inner_codomain, unification_id, inner) - } + Type::Constructor(_, _) => true, - Type::Function(argument, result) => { - promote_type(state, codomain, unification_id, argument) - && promote_type(state, codomain, unification_id, result) - } + Type::Forall(ref binder, inner) => { + let inner_codomain = codomain.increment(); + aux(s, c, codomain, binder.kind) && aux(s, c, inner_codomain, inner) + } - Type::Integer(_) => true, + Type::Function(argument, result) => { + aux(s, c, codomain, argument) && aux(s, c, codomain, result) + } - Type::KindApplication(function, argument) => { - promote_type(state, codomain, unification_id, function) - && promote_type(state, codomain, unification_id, argument) - } + Type::Integer(_) => true, - Type::Kinded(inner, kind) => { - promote_type(state, codomain, unification_id, inner) - && promote_type(state, codomain, unification_id, kind) - } + Type::KindApplication(function, argument) => { + aux(s, c, codomain, function) && aux(s, c, codomain, argument) + } - Type::Operator(_, _) => true, + Type::Kinded(inner, kind) => aux(s, c, codomain, inner) && aux(s, c, codomain, kind), - Type::OperatorApplication(_, _, left, right) => { - promote_type(state, codomain, unification_id, left) - && promote_type(state, codomain, unification_id, right) - } + Type::Operator(_, _) => true, + + Type::OperatorApplication(_, _, left, right) => { + aux(s, c, codomain, left) && aux(s, c, codomain, right) + } - Type::Row(RowType { ref fields, tail }) => { - let fields = Arc::clone(fields); + Type::Row(RowType { ref fields, tail }) => { + let fields = Arc::clone(fields); + + for field in fields.iter() { + if !aux(s, c, codomain, field.id) { + return false; + } + } - for field in fields.iter() { - if !promote_type(state, codomain, unification_id, field.id) { + if let Some(tail) = tail + && !aux(s, c, codomain, tail) + { return false; } - } - if let Some(tail) = tail - && !promote_type(state, codomain, unification_id, tail) - { - return false; + true } - true - } - - Type::String(_, _) => true, + Type::String(_, _) => true, - Type::SynonymApplication(_, _, _, ref arguments) => { - let arguments = Arc::clone(arguments); - for argument in arguments.iter() { - if !promote_type(state, codomain, unification_id, *argument) { - return false; + Type::SynonymApplication(_, _, _, ref arguments) => { + let arguments = Arc::clone(arguments); + for argument in arguments.iter() { + if !aux(s, c, codomain, *argument) { + return false; + } } + true } - true - } - Type::Unification(solution_id) => { - let unification = state.unification.get(unification_id); - let solution = state.unification.get(solution_id); + Type::Unification(solution_id) => { + let unification = s.unification.get(c.unification_id); + let solution = s.unification.get(solution_id); - if unification_id == solution_id { - return false; - } + if c.unification_id == solution_id { + return false; + } - if unification.domain < solution.domain { - let promoted_ty = - state.fresh_unification_kinded_at(unification.domain, unification.kind); + if unification.domain < solution.domain { + let promoted_ty = + s.fresh_unification_kinded_at(unification.domain, unification.kind); - // promoted_ty is simple enough to not warrant `solve` recursion - state.unification.solve(solution_id, promoted_ty); - } + // promoted_ty is simple enough to not warrant `solve` recursion + s.unification.solve(solution_id, promoted_ty); + } - true - } + true + } - Type::Variable(ref variable) => { - // A bound variable escapes if its level >= the unification variable's domain. - // This means the variable was bound at or after the unification was created. - match variable { - Variable::Bound(level, kind) => { - let unification = state.unification.get(unification_id); - if level.0 >= unification.domain.0 { - return false; + Type::Variable(ref variable) => { + // Given a unification variable ?u created at depth C; and + // the solve depth S, the type scope size when solve was + // called; and a given variable bound at `level`, we define: + // + // level < C — safe, in scope when ?u was created + // C <= level < S — unsafe, introduced after ?u but before solving + // S <= level — safe, bound by a forall within the solution + // + // The third rule enables impredicative solutions. Forall types + // inside the solution introduce bound variables that are local + // to the solution type and don't escape. For example: + // + // Solving `?a[:1] := forall c. Maybe c` + // + // forall a. -- level 0, below C(1) → safe + // forall b. -- level 1, C=1, ?a created here + // solve at S=2 + // forall c. -- level 2, >= S(2) → solution-internal, safe + // Maybe c + // + // Without the third rule, `c` at level 2 >= C(1) would be + // rejected as escaping, breaking `?a := forall c. Maybe c`. + match variable { + Variable::Bound(level, kind) => { + if level.0 >= c.initial_codomain.0 { + // S <= level + return aux(s, c, codomain, *kind); + } + let unification = s.unification.get(c.unification_id); + if level.0 >= unification.domain.0 { + // C <= level < S + return false; + } + // level < C + aux(s, c, codomain, *kind) } - promote_type(state, codomain, unification_id, *kind) + Variable::Skolem(_, kind) => aux(s, c, codomain, *kind), + Variable::Free(_) => true, } - Variable::Skolem(_, kind) => promote_type(state, codomain, unification_id, *kind), - Variable::Free(_) => true, } - } - Type::Unknown => true, + Type::Unknown => true, + } } + + let c = PromoteContext { initial_codomain, unification_id }; + aux(state, &c, initial_codomain, solution) } /// Checks that `t1_row` is a subtype of `t2_row`, generated errors for diff --git a/tests-integration/fixtures/checking/282_higher_rank_unification/Lib.purs b/tests-integration/fixtures/checking/282_higher_rank_unification/Lib.purs new file mode 100644 index 00000000..ead39009 --- /dev/null +++ b/tests-integration/fixtures/checking/282_higher_rank_unification/Lib.purs @@ -0,0 +1,12 @@ +module Lib where + +data Maybe a = Nothing | Just a + +isJust :: forall a. Maybe a -> Boolean +isJust (Just _) = true +isJust Nothing = false + +foreign import data Fn2 :: Type -> Type -> Type -> Type +foreign import data Fn3 :: Type -> Type -> Type -> Type -> Type +foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c +foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d diff --git a/tests-integration/fixtures/checking/282_higher_rank_unification/Main.purs b/tests-integration/fixtures/checking/282_higher_rank_unification/Main.purs new file mode 100644 index 00000000..a65d9cca --- /dev/null +++ b/tests-integration/fixtures/checking/282_higher_rank_unification/Main.purs @@ -0,0 +1,16 @@ +module Main where + +import Lib (Maybe(..), isJust, Fn2, Fn3, runFn2, runFn3) + +foreign import findImpl + :: forall a b. Fn2 (forall c. Maybe c) (a -> Maybe b) (Maybe b) + +findMap :: forall a b. (a -> Maybe b) -> Maybe b +findMap = runFn2 findImpl Nothing + +foreign import findMapImpl + :: forall a b + . Fn3 (forall c. Maybe c) (forall c. Maybe c -> Boolean) (a -> Maybe b) (Maybe b) + +findMap' :: forall a b. (a -> Maybe b) -> Maybe b +findMap' = runFn3 findMapImpl Nothing isJust diff --git a/tests-integration/fixtures/checking/282_higher_rank_unification/Main.snap b/tests-integration/fixtures/checking/282_higher_rank_unification/Main.snap new file mode 100644 index 00000000..652139c2 --- /dev/null +++ b/tests-integration/fixtures/checking/282_higher_rank_unification/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +findImpl :: + forall (a :: Type) (b :: Type). + Fn2 + (forall (c :: Type). Maybe (c :: Type)) + ((a :: Type) -> Maybe (b :: Type)) + (Maybe (b :: Type)) +findMap :: forall (a :: Type) (b :: Type). ((a :: Type) -> Maybe (b :: Type)) -> Maybe (b :: Type) +findMapImpl :: + forall (a :: Type) (b :: Type). + Fn3 + (forall (c :: Type). Maybe (c :: Type)) + (forall (c :: Type). Maybe (c :: Type) -> Boolean) + ((a :: Type) -> Maybe (b :: Type)) + (Maybe (b :: Type)) +findMap' :: forall (a :: Type) (b :: Type). ((a :: Type) -> Maybe (b :: Type)) -> Maybe (b :: Type) + +Types diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 67356299..dfdc19e8 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -577,3 +577,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_280_partial_case_variable_main() { run_test("280_partial_case_variable", "Main"); } #[rustfmt::skip] #[test] fn test_281_sectioned_constraint_generation_main() { run_test("281_sectioned_constraint_generation", "Main"); } + +#[rustfmt::skip] #[test] fn test_282_higher_rank_unification_main() { run_test("282_higher_rank_unification", "Main"); } From ac8f8d48cdedd00016a93cee39e3a3b09df3fec0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Feb 2026 00:13:18 +0800 Subject: [PATCH 106/386] Remove references to domain and codomain --- compiler-core/checking/src/algorithm/kind.rs | 6 +-- .../checking/src/algorithm/quantify.rs | 12 ++--- compiler-core/checking/src/algorithm/state.rs | 10 ++-- .../src/algorithm/state/unification.rs | 6 +-- .../checking/src/algorithm/type_item.rs | 2 +- .../checking/src/algorithm/unification.rs | 50 +++++++++---------- compiler-core/checking/src/core/pretty.rs | 2 +- tests-integration/tests/checking.rs | 24 ++++----- 8 files changed, 54 insertions(+), 58 deletions(-) diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index 45bf82c4..71004671 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -443,10 +443,10 @@ where Type::Function(_, result) => result, Type::Unification(unification_id) => { - let domain = state.unification.get(unification_id).domain; + let depth = state.unification.get(unification_id).depth; - let argument_u = state.fresh_unification_kinded_at(domain, context.prim.t); - let result_u = state.fresh_unification_kinded_at(domain, context.prim.t); + let argument_u = state.fresh_unification_kinded_at(depth, context.prim.t); + let result_u = state.fresh_unification_kinded_at(depth, context.prim.t); let function = state.storage.intern(Type::Function(argument_u, result_u)); let _ = unification::solve(state, context, unification_id, function); diff --git a/compiler-core/checking/src/algorithm/quantify.rs b/compiler-core/checking/src/algorithm/quantify.rs index dad258aa..629759fb 100644 --- a/compiler-core/checking/src/algorithm/quantify.rs +++ b/compiler-core/checking/src/algorithm/quantify.rs @@ -355,14 +355,14 @@ pub fn quantify_instance(state: &mut CheckState, instance: &mut Instance) -> Opt /// Builds a topological sort of the [`UniGraph`]. /// -/// This function uses the domain-based sorting of the unification variables +/// This function uses the depth-based sorting of the unification variables /// as the base for the post-order traversal. In turn, this ensures that -/// unconnected nodes are ordered by domain while connected ones are sorted +/// unconnected nodes are ordered by depth while connected ones are sorted /// topologically. The resulting [`IndexSet`] can be iterated in reverse to /// build the `forall` binders during quantification. fn ordered_toposort(graph: &UniGraph, state: &CheckState) -> Option> { let mut nodes: Vec = graph.nodes().collect(); - nodes.sort_by_key(|&id| (state.unification.get(id).domain, id)); + nodes.sort_by_key(|&id| (state.unification.get(id).depth, id)); let mut dfs = DfsPostOrder::empty(graph); let mut unsolved = IndexSet::new(); @@ -488,9 +488,9 @@ fn collect_unification(state: &mut CheckState, id: TypeId) -> UniGraph { mod tests { use super::*; - fn add_unification(state: &mut CheckState, domain: u32) -> u32 { + fn add_unification(state: &mut CheckState, depth: u32) -> u32 { let kind = state.storage.intern(Type::Unknown); - state.unification.fresh(debruijn::Size(domain), kind) + state.unification.fresh(debruijn::Size(depth), kind) } #[test] @@ -637,7 +637,7 @@ mod tests { let sorted: Vec = result.unwrap().into_iter().collect(); - // All have the same domain, + // All have the same depth, assert_eq!(sorted, vec![id3, id2, id0, id1]); } } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 7763fa20..2ef6fed8 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1069,16 +1069,16 @@ impl CheckState { /// Functions for creating unification variables. impl CheckState { - /// Creates a fresh unification variable with the provided domain and kind. - pub fn fresh_unification_kinded_at(&mut self, domain: debruijn::Size, kind: TypeId) -> TypeId { - let unification_id = self.unification.fresh(domain, kind); + /// Creates a fresh unification variable with the provided depth and kind. + pub fn fresh_unification_kinded_at(&mut self, depth: debruijn::Size, kind: TypeId) -> TypeId { + let unification_id = self.unification.fresh(depth, kind); self.storage.intern(Type::Unification(unification_id)) } /// Creates a fresh unification variable with the provided kind. pub fn fresh_unification_kinded(&mut self, kind: TypeId) -> TypeId { - let domain = self.type_scope.size(); - self.fresh_unification_kinded_at(domain, kind) + let depth = self.type_scope.size(); + self.fresh_unification_kinded_at(depth, kind) } /// Creates a fresh polykinded unification variable. diff --git a/compiler-core/checking/src/algorithm/state/unification.rs b/compiler-core/checking/src/algorithm/state/unification.rs index 50140049..0596c60b 100644 --- a/compiler-core/checking/src/algorithm/state/unification.rs +++ b/compiler-core/checking/src/algorithm/state/unification.rs @@ -8,7 +8,7 @@ pub enum UnificationState { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct UnificationEntry { - pub domain: debruijn::Size, + pub depth: debruijn::Size, pub kind: TypeId, pub state: UnificationState, } @@ -20,11 +20,11 @@ pub struct UnificationContext { } impl UnificationContext { - pub fn fresh(&mut self, domain: debruijn::Size, kind: TypeId) -> u32 { + pub fn fresh(&mut self, depth: debruijn::Size, kind: TypeId) -> u32 { let unique = self.unique; self.unique += 1; - self.entries.push(UnificationEntry { domain, kind, state: UnificationState::Unsolved }); + self.entries.push(UnificationEntry { depth, kind, state: UnificationState::Unsolved }); unique } diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index e7622f5b..853a4158 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -511,7 +511,7 @@ where }); let mut unsolved_kinds = unsolved_kinds.collect_vec(); - unsolved_kinds.sort_by_key(|&(_, id)| (state.unification.get(id).domain, id)); + unsolved_kinds.sort_by_key(|&(_, id)| (state.unification.get(id).depth, id)); let reference_type = unsolved_kinds.iter().fold(reference_type, |reference, &(kind, _)| { state.storage.intern(Type::KindApplication(reference, kind)) diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 16f910b5..3476d4c7 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -350,9 +350,9 @@ where solution = solution, }); - let codomain = state.type_scope.size(); + let solve_depth = state.type_scope.size(); - if !promote_type(state, codomain, unification_id, solution) { + if !promote_type(state, solve_depth, unification_id, solution) { crate::trace_fields!(state, context, { ?unification_id = unification_id, solution = solution, @@ -371,7 +371,7 @@ where pub fn promote_type( state: &mut CheckState, - initial_codomain: debruijn::Size, + solve_depth: debruijn::Size, unification_id: u32, solution: TypeId, ) -> bool { @@ -381,58 +381,58 @@ pub fn promote_type( /// /// Bound variables at or above this level are introduced /// by foralls within the solution and don't escape. - initial_codomain: debruijn::Size, + solve_depth: debruijn::Size, /// The unification variable being solved. unification_id: u32, } - fn aux(s: &mut CheckState, c: &PromoteContext, codomain: debruijn::Size, t: TypeId) -> bool { + fn aux(s: &mut CheckState, c: &PromoteContext, depth: debruijn::Size, t: TypeId) -> bool { let t = s.normalize_type(t); match s.storage[t] { Type::Application(function, argument) => { - aux(s, c, codomain, function) && aux(s, c, codomain, argument) + aux(s, c, depth, function) && aux(s, c, depth, argument) } Type::Constrained(constraint, inner) => { - aux(s, c, codomain, constraint) && aux(s, c, codomain, inner) + aux(s, c, depth, constraint) && aux(s, c, depth, inner) } Type::Constructor(_, _) => true, Type::Forall(ref binder, inner) => { - let inner_codomain = codomain.increment(); - aux(s, c, codomain, binder.kind) && aux(s, c, inner_codomain, inner) + let inner_depth = depth.increment(); + aux(s, c, depth, binder.kind) && aux(s, c, inner_depth, inner) } Type::Function(argument, result) => { - aux(s, c, codomain, argument) && aux(s, c, codomain, result) + aux(s, c, depth, argument) && aux(s, c, depth, result) } Type::Integer(_) => true, Type::KindApplication(function, argument) => { - aux(s, c, codomain, function) && aux(s, c, codomain, argument) + aux(s, c, depth, function) && aux(s, c, depth, argument) } - Type::Kinded(inner, kind) => aux(s, c, codomain, inner) && aux(s, c, codomain, kind), + Type::Kinded(inner, kind) => aux(s, c, depth, inner) && aux(s, c, depth, kind), Type::Operator(_, _) => true, Type::OperatorApplication(_, _, left, right) => { - aux(s, c, codomain, left) && aux(s, c, codomain, right) + aux(s, c, depth, left) && aux(s, c, depth, right) } Type::Row(RowType { ref fields, tail }) => { let fields = Arc::clone(fields); for field in fields.iter() { - if !aux(s, c, codomain, field.id) { + if !aux(s, c, depth, field.id) { return false; } } if let Some(tail) = tail - && !aux(s, c, codomain, tail) + && !aux(s, c, depth, tail) { return false; } @@ -445,7 +445,7 @@ pub fn promote_type( Type::SynonymApplication(_, _, _, ref arguments) => { let arguments = Arc::clone(arguments); for argument in arguments.iter() { - if !aux(s, c, codomain, *argument) { + if !aux(s, c, depth, *argument) { return false; } } @@ -460,9 +460,9 @@ pub fn promote_type( return false; } - if unification.domain < solution.domain { + if unification.depth < solution.depth { let promoted_ty = - s.fresh_unification_kinded_at(unification.domain, unification.kind); + s.fresh_unification_kinded_at(unification.depth, unification.kind); // promoted_ty is simple enough to not warrant `solve` recursion s.unification.solve(solution_id, promoted_ty); @@ -496,19 +496,19 @@ pub fn promote_type( // rejected as escaping, breaking `?a := forall c. Maybe c`. match variable { Variable::Bound(level, kind) => { - if level.0 >= c.initial_codomain.0 { + if level.0 >= c.solve_depth.0 { // S <= level - return aux(s, c, codomain, *kind); + return aux(s, c, depth, *kind); } let unification = s.unification.get(c.unification_id); - if level.0 >= unification.domain.0 { + if level.0 >= unification.depth.0 { // C <= level < S return false; } // level < C - aux(s, c, codomain, *kind) + aux(s, c, depth, *kind) } - Variable::Skolem(_, kind) => aux(s, c, codomain, *kind), + Variable::Skolem(_, kind) => aux(s, c, depth, *kind), Variable::Free(_) => true, } } @@ -517,8 +517,8 @@ pub fn promote_type( } } - let c = PromoteContext { initial_codomain, unification_id }; - aux(state, &c, initial_codomain, solution) + let c = PromoteContext { solve_depth, unification_id }; + aux(state, &c, solve_depth, solution) } /// Checks that `t1_row` is a subtype of `t2_row`, generated errors for diff --git a/compiler-core/checking/src/core/pretty.rs b/compiler-core/checking/src/core/pretty.rs index 679d8a1f..2bddcfa5 100644 --- a/compiler-core/checking/src/core/pretty.rs +++ b/compiler-core/checking/src/core/pretty.rs @@ -454,7 +454,7 @@ where Type::Unification(unification_id) => match source { TraversalSource::Local { state, .. } => { let unification = state.unification.get(unification_id); - arena.text(format!("?{}[{}]", unification_id, unification.domain)) + arena.text(format!("?{}[{}]", unification_id, unification.depth)) } TraversalSource::Global { .. } => arena.text(format!("?{}[]", unification_id)), }, diff --git a/tests-integration/tests/checking.rs b/tests-integration/tests/checking.rs index eab3ed53..648f6833 100644 --- a/tests-integration/tests/checking.rs +++ b/tests-integration/tests/checking.rs @@ -118,31 +118,27 @@ fn test_solve_bound() { } #[test] -fn test_solve_invalid() { +fn test_solve_escaping_variable() { let (engine, id) = empty_engine(); let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // [a :: Int] state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.int); + // ?u created at depth C = 1 let unification = state.fresh_unification_type(context); let Type::Unification(unification_id) = state.storage[unification] else { unreachable!("invariant violated"); }; - // [a :: Int, b :: String] - let level = state - .type_scope - .bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); - - let bound_b = state.bound_variable(0, context.prim.int); - let bound_a = state.bound_variable(1, context.prim.string); - let b_to_a = state.function(bound_b, bound_a); + // [a :: Int, b :: String] S = 2 + state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); - state.type_scope.unbind(level); + // b is at level 1 which is C(1) <= level(1) < S(2) + let bound_b = state.bound_variable(1, context.prim.string); - let solve_result = unification::solve(state, context, unification_id, b_to_a).unwrap(); - assert!(solve_result.is_none()); + let solve_result = unification::solve(state, context, unification_id, bound_b).unwrap(); + assert!(solve_result.is_none(), "should reject: b escapes the scope where ?u was created"); } #[test] @@ -169,9 +165,9 @@ fn test_solve_promotion() { let entries: Vec<_> = state.unification.iter().copied().collect(); for (index, entry) in entries.iter().enumerate() { let UnificationState::Solved(solution) = entry.state else { continue }; - let domain = entry.domain; + let depth = entry.depth; let solution = pretty::print_local(state, context, solution); - writeln!(snapshot, "?{index}[{domain}] := {solution}").unwrap(); + writeln!(snapshot, "?{index}[{depth}] := {solution}").unwrap(); } insta::assert_snapshot!(snapshot); From efdd701f31360203ea8d3d6b189196dbb4b0af92 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Feb 2026 15:54:34 +0800 Subject: [PATCH 107/386] Fix stale AGENTS.md --- AGENTS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index e31e6a4c..325212f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,5 +30,4 @@ PureScript compiler frontend in Rust using rowan (lossless syntax trees) and que ## Skills -Load `.claude/skills/compiler-scripts` when running integration tests. Load `.claude/skills/type-checker-tests` when implementing type checker tests. From 5432031dc4d0796f75016153960559e18f71c154 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Feb 2026 15:58:54 +0800 Subject: [PATCH 108/386] Implement synonym expansion for operator applications --- .../checking/src/algorithm/kind/synonym.rs | 50 ++++++++++++++++++- .../Main.purs | 15 ++++++ .../Main.snap | 34 +++++++++++++ .../Main.purs | 11 ++++ .../Main.snap | 32 ++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.purs create mode 100644 tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.snap create mode 100644 tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.purs create mode 100644 tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 562f19d6..33156c4f 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -12,7 +12,7 @@ use building_types::QueryResult; use files::FileId; use indexing::TypeItemId; use itertools::Itertools; -use lowering::GroupedModule; +use lowering::{GroupedModule, LoweredModule, TypeItemIr}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, substitute, transfer, unification}; @@ -325,6 +325,16 @@ enum DiscoveredSynonym { Additional { synonym: Synonym, arguments: Arc<[TypeId]>, additional: Vec }, } +fn resolve_operator_target( + lowered: &LoweredModule, + item_id: TypeItemId, +) -> Option<(FileId, TypeItemId)> { + let TypeItemIr::Operator { resolution, .. } = lowered.info.get_type_item(item_id)? else { + return None; + }; + *resolution +} + fn discover_synonym_application( state: &mut CheckState, context: &CheckContext, @@ -413,6 +423,44 @@ where } } + Type::OperatorApplication(operator_file_id, operator_item_id, left, right) => { + let resolution = if operator_file_id == context.id { + resolve_operator_target(&context.lowered, operator_item_id) + } else { + let lowered = context.queries.lowered(operator_file_id)?; + resolve_operator_target(&lowered, operator_item_id) + }; + + let Some((file_id, item_id)) = resolution else { + return Ok(None); + }; + + let Some((synonym, _)) = lookup_file_synonym(state, context, file_id, item_id)? else { + return Ok(None); + }; + + if is_recursive_synonym(context, file_id, item_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); + return Ok(None); + } + + let arguments = vec![left, right]; + + if arguments.len() != synonym.type_variables.0 as usize { + return Ok(None); + } + + if additional.is_empty() { + Ok(Some(DiscoveredSynonym::Saturated { synonym, arguments })) + } else { + Ok(Some(DiscoveredSynonym::Additional { + synonym, + arguments: Arc::from(arguments), + additional, + })) + } + } + _ => Ok(None), } } diff --git a/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.purs b/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.purs new file mode 100644 index 00000000..cc87032e --- /dev/null +++ b/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.purs @@ -0,0 +1,15 @@ +module Main where + +data Maybe a = Just a | Nothing + +type NaturalTransformation f g = forall a. f a -> g a + +infixr 4 type NaturalTransformation as ~> + +test :: Maybe ~> Maybe +test (Just a) = Just a +test Nothing = Nothing + +test' :: NaturalTransformation Maybe Maybe +test' (Just a) = Just a +test' Nothing = Nothing diff --git a/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.snap new file mode 100644 index 00000000..cd6643e0 --- /dev/null +++ b/tests-integration/fixtures/checking/283_type_operator_synonym_expansion/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Maybe ~> Maybe +test' :: NaturalTransformation Maybe Maybe + +Types +Maybe :: Type -> Type +NaturalTransformation :: + forall (t13 :: Type). ((t13 :: Type) -> Type) -> ((t13 :: Type) -> Type) -> Type +~> :: forall (t13 :: Type). ((t13 :: Type) -> Type) -> ((t13 :: Type) -> Type) -> Type + +Synonyms +NaturalTransformation = forall (t13 :: Type) (f :: (t13 :: Type) -> Type) (g :: (t13 :: Type) -> Type) (a :: (t13 :: Type)). + (f :: (t13 :: Type) -> Type) (a :: (t13 :: Type)) -> + (g :: (t13 :: Type) -> Type) (a :: (t13 :: Type)) + Quantified = :1 + Kind = :0 + Type = :2 + + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] diff --git a/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.purs b/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.purs new file mode 100644 index 00000000..f5600b31 --- /dev/null +++ b/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.purs @@ -0,0 +1,11 @@ +module Main where + +data List a = Cons a (List a) | Nil + +type Transform f g = forall a. f a -> g a + +infixr 4 type Transform as ~> + +head :: List ~> List +head (Cons a _) = Cons a Nil +head Nil = Nil diff --git a/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.snap b/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.snap new file mode 100644 index 00000000..bfd0ac9a --- /dev/null +++ b/tests-integration/fixtures/checking/284_type_operator_synonym_with_binders/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Cons :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +Nil :: forall (a :: Type). List (a :: Type) +head :: List ~> List + +Types +List :: Type -> Type +Transform :: forall (t14 :: Type). ((t14 :: Type) -> Type) -> ((t14 :: Type) -> Type) -> Type +~> :: forall (t14 :: Type). ((t14 :: Type) -> Type) -> ((t14 :: Type) -> Type) -> Type + +Synonyms +Transform = forall (t14 :: Type) (f :: (t14 :: Type) -> Type) (g :: (t14 :: Type) -> Type) (a :: (t14 :: Type)). + (f :: (t14 :: Type) -> Type) (a :: (t14 :: Type)) -> + (g :: (t14 :: Type) -> Type) (a :: (t14 :: Type)) + Quantified = :1 + Kind = :0 + Type = :2 + + +Data +List + Quantified = :0 + Kind = :0 + + +Roles +List = [Representational] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index dfdc19e8..452083b6 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -579,3 +579,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_281_sectioned_constraint_generation_main() { run_test("281_sectioned_constraint_generation", "Main"); } #[rustfmt::skip] #[test] fn test_282_higher_rank_unification_main() { run_test("282_higher_rank_unification", "Main"); } + +#[rustfmt::skip] #[test] fn test_283_type_operator_synonym_expansion_main() { run_test("283_type_operator_synonym_expansion", "Main"); } + +#[rustfmt::skip] #[test] fn test_284_type_operator_synonym_with_binders_main() { run_test("284_type_operator_synonym_with_binders", "Main"); } From bb82348b1b353cb25d448e96e6aca519a1e000f4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Feb 2026 22:56:33 +0800 Subject: [PATCH 109/386] Fix derive newtype for classes with higher-kinded arguments --- .../constraint/compiler_solved/prim_coerce.rs | 4 +- .../checking/src/algorithm/derive.rs | 102 +++++++++++++++--- .../checking/src/algorithm/derive/generic.rs | 4 +- .../checking/src/algorithm/derive/newtype.rs | 2 +- .../checking/src/algorithm/exhaustiveness.rs | 3 +- .../checking/src/algorithm/toolkit.rs | 16 +-- compiler-core/checking/src/error.rs | 1 + compiler-core/diagnostics/src/convert.rs | 6 ++ .../Main.purs | 11 ++ .../Main.snap | 31 ++++++ .../Main.purs | 13 +++ .../Main.snap | 49 +++++++++ tests-integration/tests/checking/generated.rs | 4 + 13 files changed, 222 insertions(+), 24 deletions(-) create mode 100644 tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.purs create mode 100644 tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index cf4ccf29..56dd1595 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -97,7 +97,7 @@ where && is_newtype(context, file_id, type_id)? { if is_constructor_in_scope(context, file_id, type_id)? { - let inner = derive::get_newtype_inner(state, context, file_id, type_id, left)?; + let (inner, _) = derive::get_newtype_inner(state, context, file_id, type_id, left)?; let constraint = make_coercible_constraint(state, context, inner, right); return Ok(NewtypeCoercionResult::Success(MatchInstance::Match { constraints: vec![constraint], @@ -113,7 +113,7 @@ where && is_newtype(context, file_id, type_id)? { if is_constructor_in_scope(context, file_id, type_id)? { - let inner = derive::get_newtype_inner(state, context, file_id, type_id, right)?; + let (inner, _) = derive::get_newtype_inner(state, context, file_id, type_id, right)?; let constraint = make_coercible_constraint(state, context, left, inner); return Ok(NewtypeCoercionResult::Success(MatchInstance::Match { constraints: vec![constraint], diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index 96f6832f..37c040ef 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -19,7 +19,7 @@ use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, term_item, toolkit, transfer}; -use crate::core::{Type, TypeId, debruijn}; +use crate::core::{Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; /// Input fields for [`check_derive`]. @@ -196,9 +196,50 @@ where return Ok(()); } - let inner_type = get_newtype_inner(state, context, newtype_file, newtype_id, newtype_type)?; + // Extract the inner type for a given newtype, instantiating any instance + // head arguments. For classes that expect higher-kinded arguments like + // `Eq1`, the unspecified arguments are skolemised and we keep track of + // the count for validation in the next step. + // + // derive newtype instance Eq NonZero + // + // inner_type := Int + // skolem_count := 0 + // + // derive newtype instance Eq1 (Vector n) + // + // inner_type := Array ~a + // skolem_count := 1 + // + let (inner_type, skolem_count) = + get_newtype_inner(state, context, newtype_file, newtype_id, newtype_type)?; + + // Classes like `Eq1` expect a higher-kinded `Type -> Type` argument. + // In order to derive the `Type -> Type` required to build the delegate + // constraint for `Eq1`, we expect that the inner type is a chain of + // type applications that can be peeled to reveal the expected kind. + // + // This peeling is only sound if the arguments being removed are the + // skolem variables that we introduced previously, and if we found an + // exact `skolem_count` of them. For example: + // + // derive newtype instance Eq1 (Vector n) + // + // inner_type := Array ~a + // skolem_count := 1 + // + // inner_type := Array + // delegate_constraint := Eq1 Array + // + let inner_type = if skolem_count == 0 { + inner_type + } else if let Some(inner_type) = try_peel_trailing_skolems(state, inner_type, skolem_count) { + inner_type + } else { + state.insert_error(ErrorKind::InvalidNewtypeDeriveSkolemArguments); + return Ok(()); + }; - // Build `Class t1 t2 Inner` given the constraint `Class t1 t2 Newtype` let delegate_constraint = { let class_type = state.storage.intern(Type::Constructor(input.class_file, input.class_id)); @@ -217,6 +258,32 @@ where tools::solve_and_report_constraints(state, context) } +fn try_peel_trailing_skolems( + state: &mut CheckState, + mut type_id: TypeId, + mut count: usize, +) -> Option { + safe_loop! { + if count == 0 { + break Some(type_id); + } + type_id = state.normalize_type(type_id); + if let Type::Application(function, argument) | Type::KindApplication(function, argument) = + state.storage[type_id] + { + let argument = state.normalize_type(argument); + if matches!(state.storage[argument], Type::Variable(Variable::Skolem(_, _))) { + count -= 1; + type_id = function; + } else { + break None; + } + } else { + break None; + } + } +} + pub fn extract_type_constructor( state: &mut CheckState, mut type_id: TypeId, @@ -272,30 +339,33 @@ where /// /// Newtypes have exactly one constructor with exactly one field. /// This function extracts that field type, substituting any type parameters. +/// If not enough type arguments are supplied, it skolemises the remaining +/// binders and returns the skolem count. pub fn get_newtype_inner( state: &mut CheckState, context: &CheckContext, newtype_file: FileId, newtype_id: TypeItemId, newtype_type: TypeId, -) -> QueryResult +) -> QueryResult<(TypeId, usize)> where Q: ExternalQueries, { let constructors = tools::lookup_data_constructors(context, newtype_file, newtype_id)?; let [constructor_id] = constructors[..] else { - return Ok(context.prim.unknown); + return Ok((context.prim.unknown, 0)); }; let constructor_type = lookup_local_term_type(state, context, newtype_file, constructor_id)?; let Some(constructor_type) = constructor_type else { - return Ok(context.prim.unknown); + return Ok((context.prim.unknown, 0)); }; let arguments = toolkit::extract_all_applications(state, newtype_type); - let fields = instantiate_constructor_fields(state, constructor_type, &arguments); - Ok(fields.into_iter().next().unwrap_or(context.prim.unknown)) + let (fields, skolem_count) = + instantiate_constructor_fields(state, constructor_type, &arguments); + Ok((fields.into_iter().next().unwrap_or(context.prim.unknown), skolem_count)) } /// Generates constraints for all fields of across all constructors. @@ -330,7 +400,7 @@ where let constructor_type = lookup_local_term_type(state, context, data_file, constructor_id)?; let Some(constructor_type) = constructor_type else { continue }; - let field_types = instantiate_constructor_fields(state, constructor_type, &arguments); + let (field_types, _) = instantiate_constructor_fields(state, constructor_type, &arguments); for field_type in field_types { higher_kinded::generate_constraint(state, context, field_type, class, class1); } @@ -343,7 +413,8 @@ where /// /// This function uses [`toolkit::instantiate_with_arguments`] to specialise /// the constructor type with the given type arguments, then extracts the -/// function arguments. Consider the ff: +/// function arguments, returning the fields and the number of skolems that +/// were introduced for the remaining arguments. Consider the ff: /// /// ```purescript /// data Either a b = Left a | Right b @@ -356,6 +427,10 @@ where /// /// derive instance Eq (Proxy @Type Int) /// -- Proxy :: Proxy @Type Int +/// +/// derive instance Eq1 (Vector n) +/// -- Vector :: Vector n ~a +/// -- skolem_count := 1 /// ``` /// /// The `arguments` parameter should be obtained by calling @@ -365,8 +440,9 @@ fn instantiate_constructor_fields( state: &mut CheckState, constructor_type: TypeId, arguments: &[TypeId], -) -> Vec { - let constructor = toolkit::instantiate_with_arguments(state, constructor_type, arguments); +) -> (Vec, usize) { + let (constructor, skolem_count) = + toolkit::instantiate_with_arguments(state, constructor_type, arguments); let (fields, _) = toolkit::extract_function_arguments(state, constructor); - fields + (fields, skolem_count) } diff --git a/compiler-core/checking/src/algorithm/derive/generic.rs b/compiler-core/checking/src/algorithm/derive/generic.rs index d295a580..ff7efae9 100644 --- a/compiler-core/checking/src/algorithm/derive/generic.rs +++ b/compiler-core/checking/src/algorithm/derive/generic.rs @@ -131,7 +131,9 @@ where derive::lookup_local_term_type(state, context, data_file, constructor_id)?; let field_types = if let Some(constructor_type) = constructor_type { - derive::instantiate_constructor_fields(state, constructor_type, arguments) + let (constructor_type, _) = + derive::instantiate_constructor_fields(state, constructor_type, arguments); + constructor_type } else { vec![] }; diff --git a/compiler-core/checking/src/algorithm/derive/newtype.rs b/compiler-core/checking/src/algorithm/derive/newtype.rs index 887c4030..d5d216f9 100644 --- a/compiler-core/checking/src/algorithm/derive/newtype.rs +++ b/compiler-core/checking/src/algorithm/derive/newtype.rs @@ -39,7 +39,7 @@ where return Ok(()); } - let inner_type = + let (inner_type, _) = derive::get_newtype_inner(state, context, newtype_file, newtype_id, newtype_type)?; let _ = unification::unify(state, context, wildcard_type, inner_type); diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index cbb409d3..2f334ec9 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -850,7 +850,8 @@ where { let constructor_type = derive::lookup_local_term_type(state, context, file_id, term_id)?; if let Some(constructor_type) = constructor_type { - let constructor = toolkit::instantiate_with_arguments(state, constructor_type, arguments); + let (constructor, _) = + toolkit::instantiate_with_arguments(state, constructor_type, arguments); let (fields, _) = toolkit::extract_function_arguments(state, constructor); Ok(fields) } else { diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 297de20d..33256967 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -131,9 +131,10 @@ pub fn instantiate_constrained(state: &mut CheckState, type_id: TypeId) -> TypeI /// Instantiates [`Type::Forall`] with the provided arguments. /// /// This function falls back to constructing skolem variables if there's -/// not enough arguments provided. This is primarily used to specialise -/// constructor types based on the [`Type::Application`] and [`Type::KindApplication`] -/// used in an instance head. For example: +/// not enough arguments provided. The number of skolem variables produced +/// is returned alongside the instantiated type. This is primarily used to +/// specialise constructor types based on the [`Type::Application`] and +/// [`Type::KindApplication`] used in an instance head. For example: /// /// ```purescript /// -- Proxy @Type Int @@ -146,8 +147,9 @@ pub fn instantiate_with_arguments( state: &mut CheckState, mut type_id: TypeId, arguments: impl AsRef<[TypeId]>, -) -> TypeId { +) -> (TypeId, usize) { let mut arguments_iter = arguments.as_ref().iter().copied(); + let mut skolemized = 0; safe_loop! { type_id = state.normalize_type(type_id); @@ -158,15 +160,17 @@ pub fn instantiate_with_arguments( let inner = *inner; let argument_type = arguments_iter.next().unwrap_or_else(|| { + skolemized += 1; let skolem = Variable::Skolem(binder_level, binder_kind); state.storage.intern(Type::Variable(skolem)) }); - type_id = substitute::SubstituteBound::on(state, binder_level, argument_type, inner); + type_id = + substitute::SubstituteBound::on(state, binder_level, argument_type, inner); } _ => break, } } - type_id + (type_id, skolemized) } diff --git a/compiler-core/checking/src/error.rs b/compiler-core/checking/src/error.rs index a8e9282a..02ce6a3e 100644 --- a/compiler-core/checking/src/error.rs +++ b/compiler-core/checking/src/error.rs @@ -94,6 +94,7 @@ pub enum ErrorKind { ExpectedNewtype { type_message: TypeErrorMessageId, }, + InvalidNewtypeDeriveSkolemArguments, NoInstanceFound { constraint: TypeErrorMessageId, }, diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 0b8a8af0..7d9ec68f 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -313,6 +313,12 @@ impl ToDiagnostics for CheckError { "CoercibleConstructorNotInScope", "Constructor not in scope for Coercible".to_string(), ), + ErrorKind::InvalidNewtypeDeriveSkolemArguments => ( + Severity::Error, + "InvalidNewtypeDeriveSkolemArguments", + "Cannot derive newtype instance where skolemised arguments do not appear trailing in the inner type." + .to_string(), + ), ErrorKind::RedundantPatterns { patterns } => { let patterns = patterns.join(", "); ( diff --git a/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.purs b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.purs new file mode 100644 index 00000000..c39d2fd4 --- /dev/null +++ b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Empty f where + empty :: f Int + +instance Empty Array where + empty = [] + +newtype Wrapper a = Wrapper (Array a) + +derive newtype instance Empty Wrapper diff --git a/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap new file mode 100644 index 00000000..1066cac5 --- /dev/null +++ b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int +Wrapper :: forall (a :: Type). Array (a :: Type) -> Wrapper (a :: Type) + +Types +Empty :: (Type -> Type) -> Constraint +Wrapper :: Type -> Type + +Data +Wrapper + Quantified = :0 + Kind = :0 + + +Roles +Wrapper = [Representational] + +Classes +class Empty (&0 :: Type -> Type) + +Instances +instance Empty (Array :: Type -> Type) + chain: 0 + +Derived +derive Empty (Wrapper :: Type -> Type) diff --git a/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.purs b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.purs new file mode 100644 index 00000000..6222d1a4 --- /dev/null +++ b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.purs @@ -0,0 +1,13 @@ +module Main where + +class Empty f where + empty :: f Int + +instance Empty Array where + empty = [] + +newtype Vector n a = Vector (Array a) +derive newtype instance Empty (Vector n) + +newtype InvalidVector a n = InvalidVector (Array a) +derive newtype instance Empty (InvalidVector Int) diff --git a/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap new file mode 100644 index 00000000..e4c7b00e --- /dev/null +++ b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap @@ -0,0 +1,49 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int +Vector :: + forall (t5 :: Type) (n :: (t5 :: Type)) (a :: Type). + Array (a :: Type) -> Vector @(t5 :: Type) (n :: (t5 :: Type)) (a :: Type) +InvalidVector :: + forall (t8 :: Type) (a :: Type) (n :: (t8 :: Type)). + Array (a :: Type) -> InvalidVector @(t8 :: Type) (a :: Type) (n :: (t8 :: Type)) + +Types +Empty :: (Type -> Type) -> Constraint +Vector :: forall (t5 :: Type). (t5 :: Type) -> Type -> Type +InvalidVector :: forall (t8 :: Type). Type -> (t8 :: Type) -> Type + +Data +Vector + Quantified = :1 + Kind = :0 + +InvalidVector + Quantified = :1 + Kind = :0 + + +Roles +Vector = [Phantom, Representational] +InvalidVector = [Representational, Phantom] + +Classes +class Empty (&0 :: Type -> Type) + +Instances +instance Empty (Array :: Type -> Type) + chain: 0 + +Derived +derive forall (&0 :: Type). Empty (Vector @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type -> Type) + +Diagnostics +error[InvalidNewtypeDeriveSkolemArguments]: Cannot derive newtype instance where skolemised arguments do not appear trailing in the inner type. + --> 13:1..13:50 + | +13 | derive newtype instance Empty (InvalidVector Int) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 452083b6..a6999b41 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -583,3 +583,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_283_type_operator_synonym_expansion_main() { run_test("283_type_operator_synonym_expansion", "Main"); } #[rustfmt::skip] #[test] fn test_284_type_operator_synonym_with_binders_main() { run_test("284_type_operator_synonym_with_binders", "Main"); } + +#[rustfmt::skip] #[test] fn test_285_derive_newtype_higher_kinded_main() { run_test("285_derive_newtype_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_286_invalid_vector_newtype_derive_main() { run_test("286_invalid_vector_newtype_derive", "Main"); } From 4fed490f0c5e5aeeae168c01e10747d5b0d873b4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 01:56:07 +0800 Subject: [PATCH 110/386] Add exhaustiveness checking for lambda expressions --- .../checking/src/algorithm/exhaustiveness.rs | 21 +++++++++++ compiler-core/checking/src/algorithm/term.rs | 15 +++++++- .../checking/287_lambda_partial/Main.purs | 11 ++++++ .../checking/287_lambda_partial/Main.snap | 36 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/287_lambda_partial/Main.purs create mode 100644 tests-integration/fixtures/checking/287_lambda_partial/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 2f334ec9..b778bb9b 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -892,6 +892,27 @@ pub struct ExhaustivenessReport { pub redundant: Vec, } +pub fn check_lambda_patterns( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + binders: &[lowering::BinderId], +) -> QueryResult +where + Q: ExternalQueries, +{ + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); + } + + let unconditional = + collect_unconditional_rows(state, context, &[binders], pattern_types, |binders| { + (binders, &None) + })?; + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + pub fn check_case_patterns( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 62639632..2cd6971f 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -342,7 +342,20 @@ where state.fresh_unification_type(context) }; - Ok(state.make_function(&argument_types, result_type)) + let function_type = state.make_function(&argument_types, result_type); + + let exhaustiveness = + exhaustiveness::check_lambda_patterns(state, context, &argument_types, binders)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(exhaustiveness); + + if has_missing { + let constrained_type = Type::Constrained(context.prim.partial, function_type); + Ok(state.storage.intern(constrained_type)) + } else { + Ok(function_type) + } } fn infer_case_of( diff --git a/tests-integration/fixtures/checking/287_lambda_partial/Main.purs b/tests-integration/fixtures/checking/287_lambda_partial/Main.purs new file mode 100644 index 00000000..b92323ac --- /dev/null +++ b/tests-integration/fixtures/checking/287_lambda_partial/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Partial.Unsafe (unsafePartial) + +data Maybe a = Just a | Nothing + +isJust = \(Just _) -> true +isNothing = \Nothing -> true + +unsafeIsJust = unsafePartial isJust +unsafeIsNothing = unsafePartial isNothing diff --git a/tests-integration/fixtures/checking/287_lambda_partial/Main.snap b/tests-integration/fixtures/checking/287_lambda_partial/Main.snap new file mode 100644 index 00000000..e4bbb736 --- /dev/null +++ b/tests-integration/fixtures/checking/287_lambda_partial/Main.snap @@ -0,0 +1,36 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +isJust :: forall (t4 :: Type). Partial => Maybe (t4 :: Type) -> Boolean +isNothing :: forall (t8 :: Type). Partial => Maybe (t8 :: Type) -> Boolean +unsafeIsJust :: forall (t12 :: Type). Maybe (t12 :: Type) -> Boolean +unsafeIsNothing :: forall (t16 :: Type). Maybe (t16 :: Type) -> Boolean + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 7:10..7:27 + | +7 | isJust = \(Just _) -> true + | ^~~~~~~~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Just _ + --> 8:13..8:29 + | +8 | isNothing = \Nothing -> true + | ^~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a6999b41..f2e213de 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -587,3 +587,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_285_derive_newtype_higher_kinded_main() { run_test("285_derive_newtype_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_286_invalid_vector_newtype_derive_main() { run_test("286_invalid_vector_newtype_derive", "Main"); } + +#[rustfmt::skip] #[test] fn test_287_lambda_partial_main() { run_test("287_lambda_partial", "Main"); } From d0c4a6f72ed418cc9f27eb0529c3776ff2c273f1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 02:27:13 +0800 Subject: [PATCH 111/386] Improve support partial constraints in argument position --- compiler-core/checking/src/algorithm/term.rs | 35 ++++++++++++++----- .../checking/src/algorithm/toolkit.rs | 18 ++++++++++ .../288_unsafe_partial_application/Main.purs | 21 +++++++++++ .../288_unsafe_partial_application/Main.snap | 31 ++++++++++++++++ .../289_custom_constraint_discharge/Main.purs | 15 ++++++++ .../289_custom_constraint_discharge/Main.snap | 23 ++++++++++++ tests-integration/tests/checking/generated.rs | 4 +++ 7 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 tests-integration/fixtures/checking/288_unsafe_partial_application/Main.purs create mode 100644 tests-integration/fixtures/checking/288_unsafe_partial_application/Main.snap create mode 100644 tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.purs create mode 100644 tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 2cd6971f..13cf4739 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -50,15 +50,32 @@ where // than leaking into unification. Skipped for higher-rank // arguments where constraints must match structurally. let expected = state.normalize_type(expected); - let inferred = - if matches!(state.storage[expected], Type::Forall(..) | Type::Constrained(..)) { - inferred - } else { - toolkit::instantiate_constrained(state, inferred) - }; - unification::subtype_with_mode(state, context, inferred, expected, ElaborationMode::No)?; - crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); - Ok(inferred) + if matches!(state.storage[expected], Type::Constrained(..)) { + // Peel constraints from expected as givens so they can + // discharge wanted constraints from the inferred type + // e.g. unsafePartial discharging Partial + let expected = toolkit::collect_given_constraints(state, expected); + let inferred = toolkit::instantiate_constrained(state, inferred); + unification::subtype_with_mode( + state, context, inferred, expected, ElaborationMode::No, + )?; + crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); + Ok(inferred) + } else if matches!(state.storage[expected], Type::Forall(..)) { + // Higher-rank, keep inferred as-is for structural matching. + unification::subtype_with_mode( + state, context, inferred, expected, ElaborationMode::No, + )?; + crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); + Ok(inferred) + } else { + let inferred = toolkit::instantiate_constrained(state, inferred); + unification::subtype_with_mode( + state, context, inferred, expected, ElaborationMode::No, + )?; + crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); + Ok(inferred) + } }) } diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 33256967..73bea90b 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -122,6 +122,24 @@ pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeI } } +/// Collects [`Type::Constrained`] as given constraints. +/// +/// Peels constraint layers from a type, pushing each as a given rather than +/// a wanted. Used when the expected type carries constraints that should +/// discharge wanted constraints from the inferred type e.g. `unsafePartial` +/// discharging `Partial`. +pub fn collect_given_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeId { + safe_loop! { + type_id = state.normalize_type(type_id); + if let Type::Constrained(constraint, constrained) = state.storage[type_id] { + state.constraints.push_given(constraint); + type_id = constrained; + } else { + break type_id; + } + } +} + /// [`instantiate_forall`] then [`collect_constraints`]. pub fn instantiate_constrained(state: &mut CheckState, type_id: TypeId) -> TypeId { let type_id = instantiate_forall(state, type_id); diff --git a/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.purs b/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.purs new file mode 100644 index 00000000..5500a8bf --- /dev/null +++ b/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.purs @@ -0,0 +1,21 @@ +module Main where + +import Partial.Unsafe (unsafePartial) + +data Maybe a = Just a | Nothing + +fromJust :: forall a. Partial => Maybe a -> a +fromJust (Just a) = a + +-- unsafePartial discharging Partial from an applied expression +test :: Int +test = unsafePartial (fromJust (Just 42)) + +test' = unsafePartial (fromJust (Just 42)) + +-- unsafePartial with partial application in map position +mapPartial :: (Int -> Boolean) -> Array Int -> Maybe Int +mapPartial = unsafePartial mapPartialImpl + where + mapPartialImpl :: Partial => (Int -> Boolean) -> Array Int -> Maybe Int + mapPartialImpl _ _ = Just 0 diff --git a/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.snap b/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.snap new file mode 100644 index 00000000..74e0ee7e --- /dev/null +++ b/tests-integration/fixtures/checking/288_unsafe_partial_application/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +fromJust :: forall (a :: Type). Partial => Maybe (a :: Type) -> (a :: Type) +test :: Int +test' :: Int +mapPartial :: (Int -> Boolean) -> Array Int -> Maybe Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 7:1..7:46 + | +7 | fromJust :: forall a. Partial => Maybe a -> a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.purs b/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.purs new file mode 100644 index 00000000..3542131b --- /dev/null +++ b/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.purs @@ -0,0 +1,15 @@ +module Main where + +class MyConstraint :: Constraint +class MyConstraint + +constrained :: MyConstraint => Int +constrained = 42 + +removeConstraint :: forall a. (MyConstraint => a) -> a +removeConstraint x = x + +test :: Int +test = removeConstraint constrained + +test' = removeConstraint constrained diff --git a/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.snap b/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.snap new file mode 100644 index 00000000..08e1a506 --- /dev/null +++ b/tests-integration/fixtures/checking/289_custom_constraint_discharge/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +constrained :: MyConstraint => Int +removeConstraint :: forall (a :: Type). (MyConstraint => (a :: Type)) -> (a :: Type) +test :: Int +test' :: Int + +Types +MyConstraint :: Constraint + +Classes +class MyConstraint + +Diagnostics +error[NoInstanceFound]: No instance found for: MyConstraint + --> 9:1..9:55 + | +9 | removeConstraint :: forall a. (MyConstraint => a) -> a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index f2e213de..2e1c4bb1 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -589,3 +589,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_286_invalid_vector_newtype_derive_main() { run_test("286_invalid_vector_newtype_derive", "Main"); } #[rustfmt::skip] #[test] fn test_287_lambda_partial_main() { run_test("287_lambda_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_288_unsafe_partial_application_main() { run_test("288_unsafe_partial_application", "Main"); } + +#[rustfmt::skip] #[test] fn test_289_custom_constraint_discharge_main() { run_test("289_custom_constraint_discharge", "Main"); } From ddf14e6decf2039bce8ed8f8704741f5d34fa807 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 02:41:29 +0800 Subject: [PATCH 112/386] Collect given constraints in operator positions too --- .../checking/src/algorithm/operator.rs | 7 ++++ .../290_apply_constraint_discharge/Main.purs | 26 ++++++++++++ .../290_apply_constraint_discharge/Main.snap | 34 +++++++++++++++ .../Main.purs | 22 ++++++++++ .../Main.snap | 41 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 6 files changed, 134 insertions(+) create mode 100644 tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.purs create mode 100644 tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.snap create mode 100644 tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.purs create mode 100644 tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap diff --git a/compiler-core/checking/src/algorithm/operator.rs b/compiler-core/checking/src/algorithm/operator.rs index c79f9a40..6e05dd27 100644 --- a/compiler-core/checking/src/algorithm/operator.rs +++ b/compiler-core/checking/src/algorithm/operator.rs @@ -59,6 +59,10 @@ where OperatorTree::Leaf(Some(type_id)) => match mode { OperatorKindMode::Infer => E::infer_surface(state, context, *type_id), OperatorKindMode::Check { expected_type } => { + // Peel constraints from the expected type as givens, + // so operator arguments like `unsafePartial $ expr` + // can discharge constraints like Partial properly. + let expected_type = toolkit::collect_given_constraints(state, expected_type); E::check_surface(state, context, *type_id, expected_type) } }, @@ -122,6 +126,9 @@ where E::record_branch_types(state, operator_id, left_type, right_type, result_type); if let OperatorKindMode::Check { expected_type } = mode { + // Peel constraints from the expected type as givens, + // so operator result constraints can be discharged. + let expected_type = toolkit::collect_given_constraints(state, expected_type); let _ = unification::subtype(state, context, result_type, expected_type)?; } diff --git a/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.purs b/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.purs new file mode 100644 index 00000000..4104cb66 --- /dev/null +++ b/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.purs @@ -0,0 +1,26 @@ +module Main where + +import Partial.Unsafe (unsafePartial) + +data Maybe a = Just a | Nothing + +fromJust :: forall a. Partial => Maybe a -> a +fromJust (Just a) = a + +deleteAt :: forall a. Int -> Array a -> Maybe (Array a) +deleteAt _ _ = Nothing + +apply :: forall a b. (a -> b) -> a -> b +apply f x = f x + +infixr 0 apply as $ + +-- apply ($) discharging Partial from a simple expression +test :: Int +test = unsafePartial $ fromJust (Just 42) + +test' = unsafePartial $ fromJust (Just 42) + +-- apply ($) discharging Partial from a more complex expression +test2 :: Array Int -> Array Int +test2 ys = unsafePartial $ fromJust (deleteAt 0 ys) diff --git a/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.snap b/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.snap new file mode 100644 index 00000000..90160739 --- /dev/null +++ b/tests-integration/fixtures/checking/290_apply_constraint_discharge/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +fromJust :: forall (a :: Type). Partial => Maybe (a :: Type) -> (a :: Type) +deleteAt :: forall (a :: Type). Int -> Array (a :: Type) -> Maybe (Array (a :: Type)) +apply :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) +$ :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) +test :: Int +test' :: Int +test2 :: Array Int -> Array Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 7:1..7:46 + | +7 | fromJust :: forall a. Partial => Maybe a -> a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.purs b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.purs new file mode 100644 index 00000000..07cc6e7b --- /dev/null +++ b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.purs @@ -0,0 +1,22 @@ +module Main where + +import Partial.Unsafe (unsafePartial) + +data Maybe a = Just a | Nothing + +fromJust :: forall a. Partial => Maybe a -> a +fromJust (Just a) = a + +compose :: forall a b c. (b -> c) -> (a -> b) -> a -> c +compose f g x = f (g x) + +infixr 9 compose as <<< + +toArray :: forall a. Maybe a -> Array a +toArray _ = [] + +-- compose (<<<) discharging Partial through operator chain +test :: forall a b. (Array a -> Maybe b) -> Maybe a -> b +test f = unsafePartial (fromJust <<< f <<< toArray) + +test' f = unsafePartial (fromJust <<< f <<< toArray) diff --git a/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap new file mode 100644 index 00000000..de58793b --- /dev/null +++ b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap @@ -0,0 +1,41 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +fromJust :: forall (a :: Type). Partial => Maybe (a :: Type) -> (a :: Type) +compose :: + forall (a :: Type) (b :: Type) (c :: Type). + ((b :: Type) -> (c :: Type)) -> ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (c :: Type) +<<< :: + forall (a :: Type) (b :: Type) (c :: Type). + ((b :: Type) -> (c :: Type)) -> ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (c :: Type) +toArray :: forall (a :: Type). Maybe (a :: Type) -> Array (a :: Type) +test :: + forall (a :: Type) (b :: Type). + (Array (a :: Type) -> Maybe (b :: Type)) -> Maybe (a :: Type) -> (b :: Type) +test' :: + forall (t25 :: Type) (t30 :: Type). + (Array (t30 :: Type) -> Maybe (t25 :: Type)) -> Maybe (t30 :: Type) -> (t25 :: Type) + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 7:1..7:46 + | +7 | fromJust :: forall a. Partial => Maybe a -> a + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 2e1c4bb1..164853a8 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -593,3 +593,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_288_unsafe_partial_application_main() { run_test("288_unsafe_partial_application", "Main"); } #[rustfmt::skip] #[test] fn test_289_custom_constraint_discharge_main() { run_test("289_custom_constraint_discharge", "Main"); } + +#[rustfmt::skip] #[test] fn test_290_apply_constraint_discharge_main() { run_test("290_apply_constraint_discharge", "Main"); } + +#[rustfmt::skip] #[test] fn test_291_compose_constraint_discharge_main() { run_test("291_compose_constraint_discharge", "Main"); } From 8c5510e39f496cb2a02d45b4e92e327fa4b75433 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 03:18:06 +0800 Subject: [PATCH 113/386] Pre-process higher-rank arguments to discharge constraints correctly When a constrained class method like `apply` is passed as an argument to a function expecting a higher-rank type, the constraint was leaking into unification because `subtype_with_mode` with `ElaborationMode::No` does not peel constraints. Skolemise forall and collect given constraints on the expected side, then instantiate and collect wanted constraints on the inferred side, before comparing the unwrapped body types. Co-Authored-By: Claude Opus 4.6 --- compiler-core/checking/src/algorithm/term.rs | 8 +++- .../checking/src/algorithm/toolkit.rs | 21 ++++++++++ .../Lib.purs | 18 +++++++++ .../Main.purs | 27 +++++++++++++ .../Main.snap | 39 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Lib.purs create mode 100644 tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.purs create mode 100644 tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 13cf4739..3f5c5d3d 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -62,7 +62,13 @@ where crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) } else if matches!(state.storage[expected], Type::Forall(..)) { - // Higher-rank, keep inferred as-is for structural matching. + // Higher-rank expected type. Skolemise forall and collect + // given constraints on the expected side, then instantiate and + // collect wanted constraints on the inferred side, before + // comparing the unwrapped body types. + let expected = toolkit::skolemise_forall(state, expected); + let expected = toolkit::collect_given_constraints(state, expected); + let inferred = toolkit::instantiate_constrained(state, inferred); unification::subtype_with_mode( state, context, inferred, expected, ElaborationMode::No, )?; diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 73bea90b..fc944252 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -109,6 +109,27 @@ pub fn instantiate_forall(state: &mut CheckState, mut type_id: TypeId) -> TypeId } } +/// Skolemises [`Type::Forall`] by replacing bound variables with skolem constants. +/// +/// This mirrors [`instantiate_forall`] but introduces skolem variables instead +/// of unification variables. Skolem variables are rigid, they cannot be unified +/// with other types, enforcing parametricity over the quantified variable. +pub fn skolemise_forall(state: &mut CheckState, mut type_id: TypeId) -> TypeId { + safe_loop! { + type_id = state.normalize_type(type_id); + if let Type::Forall(ref binder, inner) = state.storage[type_id] { + let binder_level = binder.level; + let binder_kind = binder.kind; + + let v = Variable::Skolem(binder_level, binder_kind); + let t = state.storage.intern(Type::Variable(v)); + type_id = substitute::SubstituteBound::on(state, binder_level, t, inner); + } else { + break type_id; + } + } +} + /// Collects [`Type::Constrained`] as wanted constraints. pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeId { safe_loop! { diff --git a/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Lib.purs b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Lib.purs new file mode 100644 index 00000000..d3e704d7 --- /dev/null +++ b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Lib.purs @@ -0,0 +1,18 @@ +module Lib where + +class Apply (f :: Type -> Type) where + apply :: forall a b. f (a -> b) -> f a -> f b + +class Functor (f :: Type -> Type) where + map :: forall a b. (a -> b) -> f a -> f b + +class Foldable (f :: Type -> Type) where + foldr :: forall a b. (a -> b -> b) -> b -> f a -> b + +data Fn2 a b c = Fn2 + +foreign import runFn2 :: forall a b c. Fn2 a b c -> a -> b -> c + +data Fn3 a b c d = Fn3 + +foreign import runFn3 :: forall a b c d. Fn3 a b c d -> a -> b -> c -> d diff --git a/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.purs b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.purs new file mode 100644 index 00000000..ad39437b --- /dev/null +++ b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.purs @@ -0,0 +1,27 @@ +module Main where + +import Lib (class Apply, apply, class Functor, map, class Foldable, foldr, Fn2, Fn3, runFn2, runFn3) + +-- When a class method like `apply` (Apply f => ...) is passed as an +-- argument to a function expecting a higher-rank type without constraints +-- (forall a' b'. m (a' -> b') -> m a' -> m b'), the constraint must be +-- peeled as a wanted rather than leaking into unification. Reproduces +-- the traverse1Impl pattern from Data.Array.NonEmpty.Internal. +foreign import impl3 + :: forall m a b + . Fn3 + (forall a' b'. (m (a' -> b') -> m a' -> m b')) + (forall a' b'. (a' -> b') -> m a' -> m b') + (a -> m b) + (m b) + +test :: forall m a b. Apply m => Functor m => (a -> m b) -> m b +test f = runFn3 impl3 apply map f + +-- Similar pattern with Foldable: fromFoldable = runFn2 impl2 foldr +foreign import impl2 + :: forall f a + . Fn2 (forall b. (a -> b -> b) -> b -> f a -> b) (f a) (Int) + +test2 :: forall f a. Foldable f => f a -> Int +test2 = runFn2 impl2 foldr diff --git a/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.snap b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.snap new file mode 100644 index 00000000..a5ca71ae --- /dev/null +++ b/tests-integration/fixtures/checking/292_higher_rank_constraint_discharge/Main.snap @@ -0,0 +1,39 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +impl3 :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Fn3 @Type @Type @Type @Type + (forall (a' :: Type) (b' :: Type). + (m :: Type -> Type) ((a' :: Type) -> (b' :: Type)) -> + (m :: Type -> Type) (a' :: Type) -> + (m :: Type -> Type) (b' :: Type)) + (forall (a' :: Type) (b' :: Type). + ((a' :: Type) -> (b' :: Type)) -> + (m :: Type -> Type) (a' :: Type) -> + (m :: Type -> Type) (b' :: Type)) + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) + ((m :: Type -> Type) (b :: Type)) +test :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Apply (m :: Type -> Type) => + Functor (m :: Type -> Type) => + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> (m :: Type -> Type) (b :: Type) +impl2 :: + forall (f :: Type -> Type) (a :: Type). + Fn2 @Type @Type @Type + (forall (b :: Type). + ((a :: Type) -> (b :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type)) + ((f :: Type -> Type) (a :: Type)) + Int +test2 :: + forall (f :: Type -> Type) (a :: Type). + Foldable (f :: Type -> Type) => (f :: Type -> Type) (a :: Type) -> Int + +Types diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 164853a8..12de95df 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -597,3 +597,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_290_apply_constraint_discharge_main() { run_test("290_apply_constraint_discharge", "Main"); } #[rustfmt::skip] #[test] fn test_291_compose_constraint_discharge_main() { run_test("291_compose_constraint_discharge", "Main"); } + +#[rustfmt::skip] #[test] fn test_292_higher_rank_constraint_discharge_main() { run_test("292_higher_rank_constraint_discharge", "Main"); } From 5b292a43459941795102e290560c1edda09a4665 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 04:25:43 +0800 Subject: [PATCH 114/386] Add support for trivially true guards and 'otherwise' --- .../checking/src/algorithm/exhaustiveness.rs | 46 ++++++++++++++++++- compiler-core/checking/src/algorithm/state.rs | 29 ++++++++++++ .../checking/266_equation_guarded/Main.snap | 5 -- .../Main.purs | 27 +++++++++++ .../Main.snap | 13 ++++++ .../checking/prelude/Data.Boolean.purs | 4 ++ tests-integration/tests/checking/generated.rs | 2 + 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.purs create mode 100644 tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.snap create mode 100644 tests-integration/fixtures/checking/prelude/Data.Boolean.purs diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index b778bb9b..039cdb19 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -961,6 +961,43 @@ where check_exhaustiveness_core(state, context, pattern_types, unconditional) } +/// Returns `true` if any alternative in a conditional guard set is trivially true. +fn has_trivially_true_alternative( + context: &CheckContext, + pattern_guarded: &[lowering::PatternGuarded], +) -> bool +where + Q: ExternalQueries, +{ + pattern_guarded.iter().any(|pg| { + !pg.pattern_guards.is_empty() + && pg.pattern_guards.iter().all(|g| is_trivially_true_guard(context, g)) + }) +} + +/// Returns `true` if the guard is `true` or `otherwise` from `Data.Boolean`. +fn is_trivially_true_guard(context: &CheckContext, guard: &lowering::PatternGuard) -> bool +where + Q: ExternalQueries, +{ + if guard.binder.is_some() { + return false; + } + let Some(expr_id) = guard.expression else { + return false; + }; + let Some(kind) = context.lowered.info.get_expression_kind(expr_id) else { + return false; + }; + match kind { + lowering::ExpressionKind::Boolean { boolean: true } => true, + lowering::ExpressionKind::Variable { + resolution: Some(lowering::TermVariableResolution::Reference(file_id, term_id)), + } => context.known_terms.otherwise == Some((*file_id, *term_id)), + _ => false, + } +} + fn collect_unconditional_rows( state: &mut CheckState, context: &CheckContext, @@ -976,8 +1013,13 @@ where for item in items { let (binders, guarded) = to_binders(item); - if !matches!(guarded, Some(lowering::GuardedExpression::Unconditional { .. }) | None) { - continue; + match guarded { + Some(lowering::GuardedExpression::Unconditional { .. }) | None => {} + Some(lowering::GuardedExpression::Conditionals { pattern_guarded }) => { + if !has_trivially_true_alternative(context, pattern_guarded) { + continue; + } + } } let mut pattern_row = vec![]; diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 2ef6fed8..173f8174 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -372,6 +372,7 @@ where pub prim_coerce: PrimCoerceCore, pub prim_type_error: PrimTypeErrorCore, pub known_types: KnownTypesCore, + pub known_terms: KnownTermsCore, pub known_reflectable: KnownReflectableCore, pub known_generic: Option, @@ -413,6 +414,7 @@ where let prim_coerce = PrimCoerceCore::collect(queries)?; let prim_type_error = PrimTypeErrorCore::collect(queries, state)?; let known_types = KnownTypesCore::collect(queries)?; + let known_terms = KnownTermsCore::collect(queries)?; let known_reflectable = KnownReflectableCore::collect(queries, &mut state.storage)?; let known_generic = KnownGeneric::collect(queries, &mut state.storage)?; let resolved = queries.resolved(id)?; @@ -431,6 +433,7 @@ where prim_coerce, prim_type_error, known_types, + known_terms, known_reflectable, known_generic, id, @@ -756,6 +759,21 @@ impl PrimTypeErrorCore { } } +fn fetch_known_term( + queries: &impl ExternalQueries, + m: &str, + n: &str, +) -> QueryResult> { + let Some(file_id) = queries.module_file(m) else { + return Ok(None); + }; + let resolved = queries.resolved(file_id)?; + let Some((file_id, term_id)) = resolved.exports.lookup_term(n) else { + return Ok(None); + }; + Ok(Some((file_id, term_id))) +} + fn fetch_known_type( queries: &impl ExternalQueries, m: &str, @@ -912,6 +930,17 @@ impl KnownGeneric { } } +pub struct KnownTermsCore { + pub otherwise: Option<(FileId, indexing::TermItemId)>, +} + +impl KnownTermsCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let otherwise = fetch_known_term(queries, "Data.Boolean", "otherwise")?; + Ok(KnownTermsCore { otherwise }) + } +} + impl CheckState { /// Executes the given closure with a term binding group in scope. /// diff --git a/tests-integration/fixtures/checking/266_equation_guarded/Main.snap b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap index 56ecb058..ddca9dbd 100644 --- a/tests-integration/fixtures/checking/266_equation_guarded/Main.snap +++ b/tests-integration/fixtures/checking/266_equation_guarded/Main.snap @@ -15,8 +15,3 @@ warning[MissingPatterns]: Pattern match is not exhaustive. Missing: true | 3 | testGuarded :: Boolean -> Int | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -warning[MissingPatterns]: Pattern match is not exhaustive. Missing: _ - --> 7:1..7:34 - | -7 | testGuardedBoth :: Boolean -> Int - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.purs b/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.purs new file mode 100644 index 00000000..c8c99ad5 --- /dev/null +++ b/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.purs @@ -0,0 +1,27 @@ +module Main where + +import Data.Boolean (otherwise) + +foreign import lessThan :: Int -> Int -> Boolean + +test :: Int -> Int +test x = case x of + n + | lessThan n 0 -> 0 + | otherwise -> n + +test' x = case x of + n + | lessThan n 0 -> 0 + | otherwise -> n + +test2 :: Int -> Int +test2 x = case x of + n + | lessThan n 0 -> 0 + | true -> n + +test2' x = case x of + n + | lessThan n 0 -> 0 + | true -> n diff --git a/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.snap b/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.snap new file mode 100644 index 00000000..5d7d8f55 --- /dev/null +++ b/tests-integration/fixtures/checking/293_exhaustive_guards_otherwise_true/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +lessThan :: Int -> Int -> Boolean +test :: Int -> Int +test' :: Int -> Int +test2 :: Int -> Int +test2' :: Int -> Int + +Types diff --git a/tests-integration/fixtures/checking/prelude/Data.Boolean.purs b/tests-integration/fixtures/checking/prelude/Data.Boolean.purs new file mode 100644 index 00000000..75a3e8e6 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Data.Boolean.purs @@ -0,0 +1,4 @@ +module Data.Boolean where + +otherwise :: Boolean +otherwise = true diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 12de95df..7dd6de10 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -599,3 +599,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_291_compose_constraint_discharge_main() { run_test("291_compose_constraint_discharge", "Main"); } #[rustfmt::skip] #[test] fn test_292_higher_rank_constraint_discharge_main() { run_test("292_higher_rank_constraint_discharge", "Main"); } + +#[rustfmt::skip] #[test] fn test_293_exhaustive_guards_otherwise_true_main() { run_test("293_exhaustive_guards_otherwise_true", "Main"); } From 3895e2c1c6971b4e3872bc23737f1112654f80db Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 04:44:59 +0800 Subject: [PATCH 115/386] Fix missing resolution in operator pattern conversion --- .../src/algorithm/exhaustiveness/convert.rs | 38 +++++++++++++++++- .../Main.purs | 16 ++++++++ .../Main.snap | 39 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index e7839925..9017fc93 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -273,6 +273,14 @@ where return Ok(state.allocate_wildcard(t)); }; + // The operator_id points to itself, thus we need to follow the + // resolution to find the constructor that it actually points to. + let Some((constructor_file_id, constructor_item_id)) = + resolve_term_operator(context, file_id, item_id)? + else { + return Ok(state.allocate_wildcard(t)); + }; + let Some(OperatorBranchTypes { left, right, result }) = state.term_scope.lookup_operator_node(operator_id) else { @@ -285,9 +293,35 @@ where let right_pattern = convert_operator_tree(state, context, right_tree, right)?; let constructor = PatternConstructor::DataConstructor { - file_id, - item_id, + file_id: constructor_file_id, + item_id: constructor_item_id, fields: vec![left_pattern, right_pattern], }; + Ok(state.allocate_constructor(constructor, result)) } + +fn resolve_term_operator( + context: &CheckContext, + file_id: FileId, + item_id: TermItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let on_lowered = |lowered: &lowering::LoweredModule| { + if let Some(lowering::TermItemIr::Operator { resolution, .. }) = + lowered.info.get_term_item(item_id) + { + *resolution + } else { + None + } + }; + if file_id == context.id { + Ok(on_lowered(&context.lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(on_lowered(&lowered)) + } +} diff --git a/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.purs b/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.purs new file mode 100644 index 00000000..ac020bb0 --- /dev/null +++ b/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.purs @@ -0,0 +1,16 @@ +module Main where + +data NonEmpty a = NonEmpty a (Array a) + +infixr 5 NonEmpty as :| + +test1 (x :| _) = x + +test2 (NonEmpty x _) = x + +data List a = Cons a (List a) | Nil + +infixr 5 Cons as : + +test3 = case _ of + (x : _) -> x diff --git a/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.snap b/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.snap new file mode 100644 index 00000000..da34b2bb --- /dev/null +++ b/tests-integration/fixtures/checking/294_exhaustive_operator_constructor/Main.snap @@ -0,0 +1,39 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +NonEmpty :: forall (a :: Type). (a :: Type) -> Array (a :: Type) -> NonEmpty (a :: Type) +:| :: forall (a :: Type). (a :: Type) -> Array (a :: Type) -> NonEmpty (a :: Type) +test1 :: forall (t5 :: Type). NonEmpty (t5 :: Type) -> (t5 :: Type) +test2 :: forall (t8 :: Type). NonEmpty (t8 :: Type) -> (t8 :: Type) +Cons :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +Nil :: forall (a :: Type). List (a :: Type) +: :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +test3 :: forall (t12 :: Type). Partial => List (t12 :: Type) -> (t12 :: Type) + +Types +NonEmpty :: Type -> Type +List :: Type -> Type + +Data +NonEmpty + Quantified = :0 + Kind = :0 + +List + Quantified = :0 + Kind = :0 + + +Roles +NonEmpty = [Representational] +List = [Representational] + +Diagnostics +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nil + --> 15:9..16:15 + | +15 | test3 = case _ of + | ^~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 7dd6de10..28db7245 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -601,3 +601,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_292_higher_rank_constraint_discharge_main() { run_test("292_higher_rank_constraint_discharge", "Main"); } #[rustfmt::skip] #[test] fn test_293_exhaustive_guards_otherwise_true_main() { run_test("293_exhaustive_guards_otherwise_true", "Main"); } + +#[rustfmt::skip] #[test] fn test_294_exhaustive_operator_constructor_main() { run_test("294_exhaustive_operator_constructor", "Main"); } From 663d5613c27bfed5db9c736c598ec63f297d2b1b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 05:21:36 +0800 Subject: [PATCH 116/386] Allow given constraints to emit equalities for stuck positions match_given_instances required functional dependencies to cover stuck unification variable positions before accepting a match. This gate is correct for instance matching but wrong for given constraints, which are authoritative as they come from signatures and superclass elaboration. The bug surfaced as a spurious NoInstanceFound when a where binding used superclass methods like pure from Applicative while the outer function only had a deeper constraint like MonadRec. The binding's type variable hadn't unified with the outer skolem yet, making the position stuck, and the functional dependency check rejected it. Co-Authored-By: Claude Opus 4.6 --- .../checking/src/algorithm/constraint.rs | 23 ++++----------- .../Main.purs | 28 +++++++++++++++++++ .../Main.snap | 18 ++++++++++++ .../checking/prelude/Control.Monad.Rec.purs | 6 ++++ tests-integration/tests/checking/generated.rs | 2 ++ 5 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.purs create mode 100644 tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.snap create mode 100644 tests-integration/fixtures/checking/prelude/Control.Monad.Rec.purs diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 9acb218a..2ef6bb40 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -942,7 +942,7 @@ where /// Matches a wanted constraint to given constraints. fn match_given_instances( state: &mut CheckState, - context: &CheckContext, + _context: &CheckContext, wanted: &ConstraintApplication, given: &[ConstraintApplication], ) -> QueryResult> @@ -958,7 +958,6 @@ where continue; } - let mut match_results = Vec::with_capacity(wanted.arguments.len()); let mut stuck_positions = vec![]; for (index, (&wanted_argument, &given_argument)) in @@ -973,24 +972,12 @@ where if matches!(match_result, MatchType::Stuck) { stuck_positions.push(index); } - - match_results.push(match_result); - } - - if stuck_positions.is_empty() { - return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); - } - - if !can_determine_stuck( - context, - wanted.file_id, - wanted.item_id, - &match_results, - &stuck_positions, - )? { - continue 'given; } + // Given constraints are valid by construction (from signatures and + // superclass elaboration). When a unification variable makes a position + // stuck, it is safe to emit an equality rather than requiring functional + // dependencies to cover it — the given constraint is authoritative. let equalities = stuck_positions.iter().map(|&index| { let wanted = wanted.arguments[index]; let given = given.arguments[index]; diff --git a/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.purs b/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.purs new file mode 100644 index 00000000..2836f830 --- /dev/null +++ b/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.purs @@ -0,0 +1,28 @@ +module Main where + +import Control.Applicative (class Applicative, pure) +import Control.Bind (class Bind, bind) +import Control.Monad (class Monad) +import Control.Monad.Rec (class MonadRec, tailRecM) +import Data.Functor (class Functor, map) + +-- Where-binding uses `pure` with the outer MonadRec constraint. +-- The where-binding's type variable is only unified with the +-- outer skolem after its body is checked, so the constraint +-- solver must emit equalities for stuck given positions. +test :: forall m a. MonadRec m => a -> m a +test a = go a + where + go x = pure x + +-- needs Bind, via MonadRec => Monad => Bind +test2 :: forall m a. MonadRec m => m a -> m a +test2 ma = go ma + where + go x = bind x pure + +-- needs Functor, via MonadRec => Monad => Apply => Functor +test3 :: forall m. MonadRec m => m Int -> m Int +test3 mi = go mi + where + go x = map (\y -> y) x diff --git a/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.snap b/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.snap new file mode 100644 index 00000000..3ce20581 --- /dev/null +++ b/tests-integration/fixtures/checking/295_superclass_entailment_where_binding/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: + forall (m :: Type -> Type) (a :: Type). + MonadRec (m :: Type -> Type) => (a :: Type) -> (m :: Type -> Type) (a :: Type) +test2 :: + forall (m :: Type -> Type) (a :: Type). + MonadRec (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> (m :: Type -> Type) (a :: Type) +test3 :: + forall (m :: Type -> Type). + MonadRec (m :: Type -> Type) => (m :: Type -> Type) Int -> (m :: Type -> Type) Int + +Types diff --git a/tests-integration/fixtures/checking/prelude/Control.Monad.Rec.purs b/tests-integration/fixtures/checking/prelude/Control.Monad.Rec.purs new file mode 100644 index 00000000..4483bc38 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Control.Monad.Rec.purs @@ -0,0 +1,6 @@ +module Control.Monad.Rec where + +import Control.Monad (class Monad) + +class Monad m <= MonadRec m where + tailRecM :: forall a b. (a -> m b) -> a -> m b diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 28db7245..d54bcc7e 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -603,3 +603,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_293_exhaustive_guards_otherwise_true_main() { run_test("293_exhaustive_guards_otherwise_true", "Main"); } #[rustfmt::skip] #[test] fn test_294_exhaustive_operator_constructor_main() { run_test("294_exhaustive_operator_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_295_superclass_entailment_where_binding_main() { run_test("295_superclass_entailment_where_binding", "Main"); } From 1f2c3b0a8cd343bf740fd9f9920863a337f9cdd3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 13:41:29 +0800 Subject: [PATCH 117/386] Expand type synonyms in function application --- compiler-core/checking/src/algorithm/term.rs | 2 +- .../Main.purs | 13 ++++++++ .../Main.snap | 32 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.purs create mode 100644 tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 3f5c5d3d..44836a28 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1545,7 +1545,7 @@ where F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { crate::trace_fields!(state, context, { function = function_t }); - let function_t = state.normalize_type(function_t); + let function_t = kind::synonym::normalize_expand_type(state, context, function_t)?; match state.storage[function_t] { // Check that `argument_id :: argument_type` Type::Function(argument_type, result_type) => { diff --git a/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.purs b/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.purs new file mode 100644 index 00000000..51a8a241 --- /dev/null +++ b/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.purs @@ -0,0 +1,13 @@ +module Main where + +type Transform f g = forall a. f a -> g a + +infixr 4 type Transform as ~> + +data Box a = Box a + +unbox :: Box ~> Array +unbox (Box a) = [a] + +test :: Array Int +test = unbox (Box 1) diff --git a/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.snap b/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.snap new file mode 100644 index 00000000..3d503d7f --- /dev/null +++ b/tests-integration/fixtures/checking/296_type_operator_synonym_in_application/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) +unbox :: Box ~> Array +test :: Array Int + +Types +Transform :: forall (t12 :: Type). ((t12 :: Type) -> Type) -> ((t12 :: Type) -> Type) -> Type +~> :: forall (t12 :: Type). ((t12 :: Type) -> Type) -> ((t12 :: Type) -> Type) -> Type +Box :: Type -> Type + +Synonyms +Transform = forall (t12 :: Type) (f :: (t12 :: Type) -> Type) (g :: (t12 :: Type) -> Type) (a :: (t12 :: Type)). + (f :: (t12 :: Type) -> Type) (a :: (t12 :: Type)) -> + (g :: (t12 :: Type) -> Type) (a :: (t12 :: Type)) + Quantified = :1 + Kind = :0 + Type = :2 + + +Data +Box + Quantified = :0 + Kind = :0 + + +Roles +Box = [Representational] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index d54bcc7e..44323555 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -605,3 +605,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_294_exhaustive_operator_constructor_main() { run_test("294_exhaustive_operator_constructor", "Main"); } #[rustfmt::skip] #[test] fn test_295_superclass_entailment_where_binding_main() { run_test("295_superclass_entailment_where_binding", "Main"); } + +#[rustfmt::skip] #[test] fn test_296_type_operator_synonym_in_application_main() { run_test("296_type_operator_synonym_in_application", "Main"); } From 3b490283089b813a25c41a882c44327d7747c5dd Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 14:17:41 +0800 Subject: [PATCH 118/386] Add application rule for application-based functions --- compiler-core/checking/src/algorithm/term.rs | 18 ++++++ .../Main.purs | 28 +++++++++ .../Main.snap | 63 +++++++++++++++++++ .../checking/prelude/Data.Semigroupoid.purs | 9 +++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 120 insertions(+) create mode 100644 tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.purs create mode 100644 tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap create mode 100644 tests-integration/fixtures/checking/prelude/Data.Semigroupoid.purs diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 44836a28..1ff2b391 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1602,6 +1602,24 @@ where ) } + // Application(Application(f, a), b) as Function(a, b) + Type::Application(partial, result_type) => { + let partial = state.normalize_type(partial); + if let Type::Application(constructor, argument_type) = state.storage[partial] { + let constructor = state.normalize_type(constructor); + if constructor == context.prim.function { + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + if let Type::Unification(unification_id) = state.storage[constructor] { + state.unification.solve(unification_id, context.prim.function); + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + } + Ok(context.prim.unknown) + } + _ => Ok(context.prim.unknown), } } diff --git a/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.purs b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.purs new file mode 100644 index 00000000..cc32af41 --- /dev/null +++ b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.purs @@ -0,0 +1,28 @@ +module Main where + +import Data.Semigroupoid ((<<<)) + +data Maybe a = Just a | Nothing + +-- When `<<<` solves its type variable `p` to `Function`, the result +-- type is `Application(Application(Function, a), b)` rather than +-- the native `Function(a, b)`. This must be decomposed correctly +-- during function-application checking. + +class Foldable f where + foldMap :: forall a m. (a -> m) -> f a -> m + +class Foldable f <= FoldableWithIndex i f where + foldMapWithIndex :: forall a m. (i -> a -> m) -> f a -> m + foldlWithIndex :: forall a b. (i -> b -> a -> b) -> b -> f a -> b + foldrWithIndex :: forall a b. (i -> a -> b -> b) -> b -> f a -> b + +data NonEmpty f a = NonEmpty a (f a) + +instance Foldable f => Foldable (NonEmpty f) where + foldMap f (NonEmpty a fa) = f a + +instance FoldableWithIndex i f => FoldableWithIndex (Maybe i) (NonEmpty f) where + foldMapWithIndex f (NonEmpty a fa) = f Nothing a + foldlWithIndex f b (NonEmpty a fa) = foldlWithIndex (f <<< Just) (f Nothing b a) fa + foldrWithIndex f b (NonEmpty a fa) = f Nothing a (foldrWithIndex (f <<< Just) b fa) diff --git a/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap new file mode 100644 index 00000000..e63599b1 --- /dev/null +++ b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap @@ -0,0 +1,63 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +foldMap :: + forall (f :: Type -> Type) (a :: Type) (m :: Type). + Foldable (f :: Type -> Type) => + ((a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) +foldMapWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (m :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) +foldlWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (b :: Type) -> (a :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) +foldrWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (b :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) +NonEmpty :: + forall (f :: Type -> Type) (a :: Type). + (a :: Type) -> (f :: Type -> Type) (a :: Type) -> NonEmpty (f :: Type -> Type) (a :: Type) + +Types +Maybe :: Type -> Type +Foldable :: (Type -> Type) -> Constraint +FoldableWithIndex :: Type -> (Type -> Type) -> Constraint +NonEmpty :: (Type -> Type) -> Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + +NonEmpty + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] +NonEmpty = [Representational, Nominal] + +Classes +class Foldable (&0 :: Type -> Type) +class Foldable (&1 :: Type -> Type) <= FoldableWithIndex (&0 :: Type) (&1 :: Type -> Type) + +Instances +instance Foldable (&0 :: Type -> Type) => Foldable (NonEmpty (&0 :: Type -> Type) :: Type -> Type) + chain: 0 +instance FoldableWithIndex (&0 :: Type) (&1 :: Type -> Type) => FoldableWithIndex (Maybe (&0 :: Type) :: Type) (NonEmpty (&1 :: Type -> Type) :: Type -> Type) + chain: 0 diff --git a/tests-integration/fixtures/checking/prelude/Data.Semigroupoid.purs b/tests-integration/fixtures/checking/prelude/Data.Semigroupoid.purs new file mode 100644 index 00000000..299046dd --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Data.Semigroupoid.purs @@ -0,0 +1,9 @@ +module Data.Semigroupoid where + +class Semigroupoid a where + compose :: forall b c d. a c d -> a b c -> a b d + +infixr 9 compose as <<< + +instance semigroupoidFn :: Semigroupoid (->) where + compose f g x = f (g x) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 44323555..a76537da 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -607,3 +607,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_295_superclass_entailment_where_binding_main() { run_test("295_superclass_entailment_where_binding", "Main"); } #[rustfmt::skip] #[test] fn test_296_type_operator_synonym_in_application_main() { run_test("296_type_operator_synonym_in_application", "Main"); } + +#[rustfmt::skip] #[test] fn test_297_applied_function_type_decomposition_main() { run_test("297_applied_function_type_decomposition", "Main"); } From d773e9bea74b23eadd2fed7f2b21bfae9643d2fc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Feb 2026 14:47:02 +0800 Subject: [PATCH 119/386] Improve function decomposition for operator checking --- .../checking/src/algorithm/operator.rs | 52 +++++++++++++++++-- .../298_operator_alias_class_method/Main.purs | 12 +++++ .../298_operator_alias_class_method/Main.snap | 16 ++++++ .../checking/prelude/Control.Category.purs | 9 ++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 tests-integration/fixtures/checking/298_operator_alias_class_method/Main.purs create mode 100644 tests-integration/fixtures/checking/298_operator_alias_class_method/Main.snap create mode 100644 tests-integration/fixtures/checking/prelude/Control.Category.purs diff --git a/compiler-core/checking/src/algorithm/operator.rs b/compiler-core/checking/src/algorithm/operator.rs index 6e05dd27..1e6dfd95 100644 --- a/compiler-core/checking/src/algorithm/operator.rs +++ b/compiler-core/checking/src/algorithm/operator.rs @@ -114,12 +114,12 @@ where let operator_type = toolkit::instantiate_forall(state, operator_type); let operator_type = toolkit::collect_constraints(state, operator_type); - let Type::Function(left_type, operator_type) = state.storage[operator_type] else { + let Some((left_type, operator_type)) = decompose_function(state, context, operator_type)? + else { return Ok(unknown); }; - let operator_type = state.normalize_type(operator_type); - let Type::Function(right_type, result_type) = state.storage[operator_type] else { + let Some((right_type, result_type)) = decompose_function(state, context, operator_type)? else { return Ok(unknown); }; @@ -151,6 +151,52 @@ where Ok(E::build(state, context, operator, (left, right), result_type)) } +/// Decompose a type into `(argument, result)` as if it were a function type. +/// +/// This function handles three representations: +/// - `Type::Function(a, b)`, the standard function representation +/// - `Type::Application(Application(f, a), b)`, the application-based form +/// - `Type::Unification(u)`, which requires function type synthesis +fn decompose_function( + state: &mut CheckState, + context: &CheckContext, + t: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + match state.storage[t] { + Type::Function(argument, result) => Ok(Some((argument, result))), + + Type::Unification(unification_id) => { + let argument = state.fresh_unification_type(context); + let result = state.fresh_unification_type(context); + + let function = state.storage.intern(Type::Function(argument, result)); + state.unification.solve(unification_id, function); + + Ok(Some((argument, result))) + } + + Type::Application(partial, result) => { + let partial = state.normalize_type(partial); + if let Type::Application(constructor, argument) = state.storage[partial] { + let constructor = state.normalize_type(constructor); + if constructor == context.prim.function { + return Ok(Some((argument, result))); + } + if let Type::Unification(unification_id) = state.storage[constructor] { + state.unification.solve(unification_id, context.prim.function); + return Ok(Some((argument, result))); + } + } + Ok(None) + } + + _ => Ok(None), + } +} + pub trait IsOperator: IsElement { type ItemId: Copy; type Elaborated: Copy; diff --git a/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.purs b/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.purs new file mode 100644 index 00000000..81bda117 --- /dev/null +++ b/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Control.Category (class Category, identity) + +-- Operator alias for a class method whose return type +-- is an applied type variable (a t t), not a Function. +infixl 4 identity as <<$>> + +test :: (Int -> Int) -> Int -> Int +test f x = f <<$>> x + +test' f x = f <<$>> x diff --git a/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.snap b/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.snap new file mode 100644 index 00000000..bd9bfb5e --- /dev/null +++ b/tests-integration/fixtures/checking/298_operator_alias_class_method/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +<<$>> :: + forall (t2 :: Type) (a :: (t2 :: Type) -> (t2 :: Type) -> Type) (t :: (t2 :: Type)). + Category (a :: (t2 :: Type) -> (t2 :: Type) -> Type) => + (a :: (t2 :: Type) -> (t2 :: Type) -> Type) (t :: (t2 :: Type)) (t :: (t2 :: Type)) +test :: (Int -> Int) -> Int -> Int +test' :: + forall (t8 :: Type) (t12 :: Type). + ((t12 :: Type) -> (t8 :: Type)) -> (t12 :: Type) -> (t8 :: Type) + +Types diff --git a/tests-integration/fixtures/checking/prelude/Control.Category.purs b/tests-integration/fixtures/checking/prelude/Control.Category.purs new file mode 100644 index 00000000..7d3c8213 --- /dev/null +++ b/tests-integration/fixtures/checking/prelude/Control.Category.purs @@ -0,0 +1,9 @@ +module Control.Category where + +import Data.Semigroupoid (class Semigroupoid) + +class Semigroupoid a <= Category a where + identity :: forall t. a t t + +instance categoryFn :: Category (->) where + identity x = x diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a76537da..ef37e477 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -609,3 +609,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_296_type_operator_synonym_in_application_main() { run_test("296_type_operator_synonym_in_application", "Main"); } #[rustfmt::skip] #[test] fn test_297_applied_function_type_decomposition_main() { run_test("297_applied_function_type_decomposition", "Main"); } + +#[rustfmt::skip] #[test] fn test_298_operator_alias_class_method_main() { run_test("298_operator_alias_class_method", "Main"); } From 92006238e26a07c749bbdcb6bca4ae69bc2622a7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 00:05:56 +0800 Subject: [PATCH 120/386] Create separate phase for constraint solving in derived instances This adds the check_derive_members phase, which implements constraint solving and implementation generation based on the result of check_derive_heads. This introduces symmetry with check_instance_heads where a user-defined instance head's constraints are solved in check_instance_members where they're actually used. Co-Authored-By: Claude Opus 4.6 --- compiler-core/checking/src/algorithm.rs | 25 +- .../checking/src/algorithm/derive.rs | 285 +++++++++++++----- .../src/algorithm/derive/contravariant.rs | 34 +-- .../checking/src/algorithm/derive/eq1.rs | 51 +--- .../checking/src/algorithm/derive/foldable.rs | 34 +-- .../checking/src/algorithm/derive/functor.rs | 34 +-- .../checking/src/algorithm/derive/generic.rs | 16 +- .../checking/src/algorithm/derive/newtype.rs | 17 +- .../checking/src/algorithm/derive/tools.rs | 29 +- .../src/algorithm/derive/traversable.rs | 34 +-- .../checking/src/algorithm/derive/variance.rs | 1 + compiler-core/checking/src/algorithm/term.rs | 18 +- .../Main.snap | 10 +- .../checking/167_derive_eq_1/Main.snap | 2 +- .../checking/168_derive_ord_1/Main.snap | 4 +- .../172_derive_generic_simple/Main.snap | 1 + .../Main.purs | 11 + .../Main.snap | 32 ++ tests-integration/tests/checking/generated.rs | 2 + 19 files changed, 400 insertions(+), 240 deletions(-) create mode 100644 tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.purs create mode 100644 tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.snap diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 6004cbff..e5cbfef2 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -86,9 +86,10 @@ pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryRes check_term_signatures(&mut state, &context)?; check_instance_heads(&mut state, &context)?; - check_derive_heads(&mut state, &context)?; + let derive_results = check_derive_heads(&mut state, &context)?; check_value_groups(&mut state, &context)?; check_instance_members(&mut state, &context)?; + check_derive_members(&mut state, &context, &derive_results)?; Ok(state.checked) } @@ -282,7 +283,7 @@ where fn check_derive_heads( state: &mut state::CheckState, context: &state::CheckContext, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -291,6 +292,8 @@ where Scc::Mutual(items) => items.as_slice(), }); + let mut results = vec![]; + for &item_id in items { let Some(TermItemIr::Derive { newtype, constraints, arguments, resolution }) = context.lowered.info.get_term_item(item_id) @@ -316,9 +319,25 @@ where is_newtype: *newtype, }; - derive::check_derive(state, context, check_derive)?; + if let Some(result) = derive::check_derive_head(state, context, check_derive)? { + results.push(result); + } } + Ok(results) +} + +fn check_derive_members( + state: &mut state::CheckState, + context: &state::CheckContext, + derive_results: &[derive::DeriveHeadResult], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for result in derive_results { + derive::check_derive_member(state, context, result)?; + } Ok(()) } diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index 37c040ef..c5428340 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -14,15 +14,17 @@ mod variance; use building_types::QueryResult; use files::FileId; use indexing::{DeriveId, TermItemId, TypeItemId}; +use itertools::Itertools; use crate::ExternalQueries; +use crate::algorithm::derive::variance::VarianceConfig; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, term_item, toolkit, transfer}; use crate::core::{Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; -/// Input fields for [`check_derive`]. +/// Input fields for [`check_derive_head`]. pub struct CheckDerive<'a> { pub item_id: TermItemId, pub derive_id: DeriveId, @@ -33,32 +35,87 @@ pub struct CheckDerive<'a> { pub is_newtype: bool, } -/// Checks a derived instance. -pub fn check_derive( +/// Determines how [`check_derive_member`] generates constraints. +enum DeriveStrategy { + /// [`generate_field_constraints`] strategy. + /// + /// * Eq + /// * Ord + FieldConstraints { + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + class: (FileId, TypeItemId), + }, + /// [`generate_variance_constraints`] strategy. + /// + /// * Functor, Bifunctor + /// * Contravariant, Profunctor + /// * Foldable, Bifoldable + /// * Traversable, Bitraversable + /// + /// [`generate_variance_constraints`]: variance::generate_variance_constraints + VarianceConstraints { + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + config: VarianceConfig, + }, + /// [`generate_delegate_constraint`] strategy. + /// + /// * Eq1 + /// * Ord1 + DelegateConstraint { derived_type: TypeId, class: (FileId, TypeItemId) }, + /// `derive newtype instance` + NewtypeDeriveConstraint { delegate_constraint: TypeId }, + /// The instance head was sufficient. + /// + /// * Generic + /// * Newtype + HeadOnly, +} + +/// Carries state from [`check_derive_head`] to [`check_derive_member`]. +pub struct DeriveHeadResult { + item_id: TermItemId, + constraints: Vec, + class_file: FileId, + class_id: TypeItemId, + arguments: Vec<(TypeId, TypeId)>, + strategy: DeriveStrategy, +} + +pub fn check_derive_head( state: &mut CheckState, context: &CheckContext, input: CheckDerive<'_>, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { - state.with_local_givens(|state| check_derive_core(state, context, input)) + state.with_local_givens(|state| check_derive_head_core(state, context, input)) }) } -fn check_derive_core( +fn check_derive_head_core( state: &mut CheckState, context: &CheckContext, input: CheckDerive<'_>, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { - let CheckDerive { derive_id, constraints, arguments, class_file, class_id, is_newtype, .. } = - input; + let CheckDerive { + item_id, + derive_id, + constraints, + arguments, + class_file, + class_id, + is_newtype, + } = input; - // Save the current size of the environment for unbinding. let size = state.type_scope.size(); let class_kind = kind::lookup_file_type(state, context, class_file, class_id)?; @@ -86,6 +143,9 @@ where core_constraints.push((inferred_type, inferred_kind)); } + let constraints = core_constraints.iter().map(|&(t, _)| t).collect_vec(); + let arguments = core_arguments.clone(); + let elaborated = tools::ElaboratedDerive { derive_id, constraints: core_constraints, @@ -94,21 +154,22 @@ where class_id, }; - if is_newtype { - check_newtype_derive(state, context, elaborated)?; + let strategy = if is_newtype { + check_newtype_derive(state, context, elaborated)? } else { let class_is = |known| Some((class_file, class_id)) == known; let known_types = &context.known_types; macro_rules! dispatch { - ($($($known:ident)|+ => $handler:path),+ $(,)?) => { - $(if $(class_is(known_types.$known))||+ { - $handler(state, context, elaborated)?; - } else)+ { - state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); - } - }; - } + ($($($known:ident)|+ => $handler:path),+ $(,)?) => { + $(if $(class_is(known_types.$known))||+ { + $handler(state, context, elaborated)? + } else)+ { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + None + } + }; + } dispatch! { eq | ord => check_derive_class, @@ -125,19 +186,25 @@ where newtype => newtype::check_derive_newtype, generic => generic::check_derive_generic, } - } + }; - // Unbind type variables bound during elaboration. state.type_scope.unbind(debruijn::Level(size.0)); - Ok(()) + Ok(strategy.map(|strategy| DeriveHeadResult { + item_id, + constraints, + class_file, + class_id, + arguments, + strategy, + })) } fn check_derive_class( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -148,96 +215,61 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let class = (input.class_file, input.class_id); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; - generate_field_constraints(state, context, data_file, data_id, derived_type, class)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::FieldConstraints { data_file, data_id, derived_type, class })) } fn check_newtype_derive( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { let [ref preceding_arguments @ .., (newtype_type, _)] = input.arguments[..] else { - return Ok(()); + return Ok(None); }; let Some((newtype_file, newtype_id)) = extract_type_constructor(state, newtype_type) else { let type_message = state.render_local_type(context, newtype_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; if newtype_file != context.id { let type_message = state.render_local_type(context, newtype_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); } if !is_newtype(context, newtype_file, newtype_id)? { let type_message = state.render_local_type(context, newtype_type); state.insert_error(ErrorKind::ExpectedNewtype { type_message }); - return Ok(()); + return Ok(None); } - // Extract the inner type for a given newtype, instantiating any instance - // head arguments. For classes that expect higher-kinded arguments like - // `Eq1`, the unspecified arguments are skolemised and we keep track of - // the count for validation in the next step. - // - // derive newtype instance Eq NonZero - // - // inner_type := Int - // skolem_count := 0 - // - // derive newtype instance Eq1 (Vector n) - // - // inner_type := Array ~a - // skolem_count := 1 - // let (inner_type, skolem_count) = get_newtype_inner(state, context, newtype_file, newtype_id, newtype_type)?; - // Classes like `Eq1` expect a higher-kinded `Type -> Type` argument. - // In order to derive the `Type -> Type` required to build the delegate - // constraint for `Eq1`, we expect that the inner type is a chain of - // type applications that can be peeled to reveal the expected kind. - // - // This peeling is only sound if the arguments being removed are the - // skolem variables that we introduced previously, and if we found an - // exact `skolem_count` of them. For example: - // - // derive newtype instance Eq1 (Vector n) - // - // inner_type := Array ~a - // skolem_count := 1 - // - // inner_type := Array - // delegate_constraint := Eq1 Array - // let inner_type = if skolem_count == 0 { inner_type } else if let Some(inner_type) = try_peel_trailing_skolems(state, inner_type, skolem_count) { inner_type } else { state.insert_error(ErrorKind::InvalidNewtypeDeriveSkolemArguments); - return Ok(()); + return Ok(None); }; let delegate_constraint = { @@ -251,13 +283,122 @@ where state.storage.intern(Type::Application(preceding_arguments, inner_type)) }; - tools::push_given_constraints(state, &input.constraints); - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; + + Ok(Some(DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint })) +} + +pub fn check_derive_member( + state: &mut CheckState, + context: &CheckContext, + result: &DeriveHeadResult, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_error_step(ErrorStep::TermDeclaration(result.item_id), |state| { + state.with_local_givens(|state| check_derive_member_core(state, context, result)) + }) +} + +fn check_derive_member_core( + state: &mut CheckState, + context: &CheckContext, + result: &DeriveHeadResult, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &constraint in &result.constraints { + state.constraints.push_given(constraint); + } + + match &result.strategy { + DeriveStrategy::FieldConstraints { data_file, data_id, derived_type, class } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + generate_field_constraints( + state, + context, + *data_file, + *data_id, + *derived_type, + *class, + )?; + } + DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + variance::generate_variance_constraints( + state, + context, + *data_file, + *data_id, + *derived_type, + *config, + )?; + } + DeriveStrategy::DelegateConstraint { derived_type, class } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + generate_delegate_constraint(state, context.prim.t, *derived_type, *class); + } + DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint } => { + state.constraints.push_wanted(*delegate_constraint); + } + DeriveStrategy::HeadOnly => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + } + } - state.constraints.push_wanted(delegate_constraint); tools::solve_and_report_constraints(state, context) } +fn generate_delegate_constraint( + state: &mut CheckState, + prim_type: TypeId, + derived_type: TypeId, + class: (FileId, TypeItemId), +) { + // Introduce a fresh skolem `~a` for the last type parameter. + let skolem_level = state.type_scope.size().0; + let skolem_level = debruijn::Level(skolem_level); + + let skolem_type = Variable::Skolem(skolem_level, prim_type); + let skolem_type = state.storage.intern(Type::Variable(skolem_type)); + + // Given `Eq ~a`, prove `Eq (Identity ~a)`. + let applied_type = state.storage.intern(Type::Application(derived_type, skolem_type)); + + let class_type = state.storage.intern(Type::Constructor(class.0, class.1)); + let given_constraint = state.storage.intern(Type::Application(class_type, skolem_type)); + state.constraints.push_given(given_constraint); + + let wanted_constraint = state.storage.intern(Type::Application(class_type, applied_type)); + state.constraints.push_wanted(wanted_constraint); +} + fn try_peel_trailing_skolems( state: &mut CheckState, mut type_id: TypeId, diff --git a/compiler-core/checking/src/algorithm/derive/contravariant.rs b/compiler-core/checking/src/algorithm/derive/contravariant.rs index be3971dc..18dc3c5b 100644 --- a/compiler-core/checking/src/algorithm/derive/contravariant.rs +++ b/compiler-core/checking/src/algorithm/derive/contravariant.rs @@ -3,17 +3,17 @@ use building_types::QueryResult; use crate::ExternalQueries; -use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::variance::{Variance, VarianceConfig}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::error::ErrorKind; -/// Checks a derive instance for Contravariant. +/// Checks a derive instance head for Contravariant. pub fn check_derive_contravariant( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -24,31 +24,28 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let contravariant = Some((input.class_file, input.class_id)); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Single((Variance::Contravariant, contravariant)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } -/// Checks a derive instance for Profunctor. +/// Checks a derive instance head for Profunctor. pub fn check_derive_profunctor( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -59,27 +56,24 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; // Profunctor: first param is contravariant, second is covariant. let contravariant = context.known_types.contravariant; let functor = context.known_types.functor; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Pair( (Variance::Contravariant, contravariant), (Variance::Covariant, functor), ); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } diff --git a/compiler-core/checking/src/algorithm/derive/eq1.rs b/compiler-core/checking/src/algorithm/derive/eq1.rs index c98779af..13255eb3 100644 --- a/compiler-core/checking/src/algorithm/derive/eq1.rs +++ b/compiler-core/checking/src/algorithm/derive/eq1.rs @@ -11,21 +11,18 @@ //! 3. Solves the constraints to determine if it's satisfiable use building_types::QueryResult; -use files::FileId; -use indexing::TypeItemId; use crate::ExternalQueries; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::core::{Type, Variable, debruijn}; use crate::error::ErrorKind; -/// Checks a derive instance for Eq1. +/// Checks a derive instance head for Eq1. pub fn check_derive_eq1( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -34,18 +31,18 @@ where class_file: input.class_file, class_id: input.class_id, }); - return Ok(()); + return Ok(None); }; check_derive_class1(state, context, input, eq) } -/// Checks a derive instance for Ord1. +/// Checks a derive instance head for Ord1. pub fn check_derive_ord1( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -54,19 +51,19 @@ where class_file: input.class_file, class_id: input.class_id, }); - return Ok(()); + return Ok(None); }; check_derive_class1(state, context, input, ord) } -/// Shared implementation for Eq1 and Ord1 derivation. +/// Shared implementation for Eq1 and Ord1 head phase. fn check_derive_class1( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, - class: (FileId, TypeItemId), -) -> QueryResult<()> + class: (files::FileId, indexing::TypeItemId), +) -> QueryResult> where Q: ExternalQueries, { @@ -77,36 +74,16 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; if derive::extract_type_constructor(state, derived_type).is_none() { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; - // Create a fresh skolem for the last type parameter. - let skolem_level = state.type_scope.size().0; - let skolem_level = debruijn::Level(skolem_level); - - let skolem_type = Variable::Skolem(skolem_level, context.prim.t); - let skolem_type = state.storage.intern(Type::Variable(skolem_type)); - - // Build the fully-applied type e.g. `Identity` -> `Identity a` - let applied_type = state.storage.intern(Type::Application(derived_type, skolem_type)); - - // Insert the given constraint `Eq a` - let class_type = state.storage.intern(Type::Constructor(class.0, class.1)); - let given_constraint = state.storage.intern(Type::Application(class_type, skolem_type)); - state.constraints.push_given(given_constraint); - - // Emit the wanted constraint `Eq (Identity a)` - let wanted_constraint = state.storage.intern(Type::Application(class_type, applied_type)); - state.constraints.push_wanted(wanted_constraint); - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::DelegateConstraint { derived_type, class })) } diff --git a/compiler-core/checking/src/algorithm/derive/foldable.rs b/compiler-core/checking/src/algorithm/derive/foldable.rs index 8995899c..df2d8a73 100644 --- a/compiler-core/checking/src/algorithm/derive/foldable.rs +++ b/compiler-core/checking/src/algorithm/derive/foldable.rs @@ -3,17 +3,17 @@ use building_types::QueryResult; use crate::ExternalQueries; -use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::variance::{Variance, VarianceConfig}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::error::ErrorKind; -/// Checks a derive instance for Foldable. +/// Checks a derive instance head for Foldable. pub fn check_derive_foldable( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -24,31 +24,28 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let foldable = Some((input.class_file, input.class_id)); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Single((Variance::Covariant, foldable)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } -/// Checks a derive instance for Bifoldable. +/// Checks a derive instance head for Bifoldable. pub fn check_derive_bifoldable( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -59,24 +56,21 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; // Bifoldable derivation emits Foldable constraints for wrapped parameters. let foldable = context.known_types.foldable; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Pair((Variance::Covariant, foldable), (Variance::Covariant, foldable)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } diff --git a/compiler-core/checking/src/algorithm/derive/functor.rs b/compiler-core/checking/src/algorithm/derive/functor.rs index 7ee41d1a..6ffa8df6 100644 --- a/compiler-core/checking/src/algorithm/derive/functor.rs +++ b/compiler-core/checking/src/algorithm/derive/functor.rs @@ -3,17 +3,17 @@ use building_types::QueryResult; use crate::ExternalQueries; -use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::variance::{Variance, VarianceConfig}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::error::ErrorKind; -/// Checks a derive instance for Functor. +/// Checks a derive instance head for Functor. pub fn check_derive_functor( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -24,31 +24,28 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let functor = Some((input.class_file, input.class_id)); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Single((Variance::Covariant, functor)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } -/// Checks a derive instance for Bifunctor. +/// Checks a derive instance head for Bifunctor. pub fn check_derive_bifunctor( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -59,24 +56,21 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; // Bifunctor derivation emits Functor constraints for wrapped parameters. let functor = context.known_types.functor; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Pair((Variance::Covariant, functor), (Variance::Covariant, functor)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } diff --git a/compiler-core/checking/src/algorithm/derive/generic.rs b/compiler-core/checking/src/algorithm/derive/generic.rs index ff7efae9..9a1a096e 100644 --- a/compiler-core/checking/src/algorithm/derive/generic.rs +++ b/compiler-core/checking/src/algorithm/derive/generic.rs @@ -15,7 +15,7 @@ use lowering::StringKind; use smol_str::SmolStr; use crate::ExternalQueries; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState, KnownGeneric}; use crate::algorithm::{toolkit, unification}; use crate::core::{Type, TypeId}; @@ -25,7 +25,7 @@ pub fn check_derive_generic( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -36,13 +36,13 @@ where expected: 2, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let Some(ref known_generic) = context.known_generic else { @@ -50,7 +50,7 @@ where class_file: input.class_file, class_id: input.class_id, }); - return Ok(()); + return Ok(None); }; let constructors = tools::lookup_data_constructors(context, data_file, data_id)?; @@ -60,11 +60,9 @@ where let _ = unification::unify(state, context, wildcard_type, generic_rep)?; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; - Ok(()) + Ok(Some(DeriveStrategy::HeadOnly)) } fn build_generic_rep( diff --git a/compiler-core/checking/src/algorithm/derive/newtype.rs b/compiler-core/checking/src/algorithm/derive/newtype.rs index d5d216f9..55a50e65 100644 --- a/compiler-core/checking/src/algorithm/derive/newtype.rs +++ b/compiler-core/checking/src/algorithm/derive/newtype.rs @@ -3,7 +3,7 @@ use building_types::QueryResult; use crate::ExternalQueries; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification; use crate::error::ErrorKind; @@ -12,7 +12,7 @@ pub fn check_derive_newtype( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -23,20 +23,20 @@ where expected: 2, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((newtype_file, newtype_id)) = derive::extract_type_constructor(state, newtype_type) else { let type_message = state.render_local_type(context, newtype_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; if !derive::is_newtype(context, newtype_file, newtype_id)? { let type_message = state.render_local_type(context, newtype_type); state.insert_error(ErrorKind::ExpectedNewtype { type_message }); - return Ok(()); + return Ok(None); } let (inner_type, _) = @@ -44,8 +44,7 @@ where let _ = unification::unify(state, context, wildcard_type, inner_type); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); - tools::solve_and_report_constraints(state, context) + tools::register_derived_instance(state, context, input)?; + + Ok(Some(DeriveStrategy::HeadOnly)) } diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 34626635..46c13458 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -37,13 +37,6 @@ pub fn emit_constraint( state.constraints.push_wanted(constraint); } -/// Pushes given constraints from the instance head onto the constraint stack. -pub fn push_given_constraints(state: &mut CheckState, constraints: &[(TypeId, TypeId)]) { - for (constraint_type, _) in constraints { - state.constraints.push_given(*constraint_type); - } -} - /// Emits wanted constraints for the superclasses of the class being derived. /// /// When deriving `Traversable (Compose f g)`, this emits: @@ -55,13 +48,14 @@ pub fn push_given_constraints(state: &mut CheckState, constraints: &[(TypeId, Ty pub fn emit_superclass_constraints( state: &mut CheckState, context: &CheckContext, - input: &ElaboratedDerive, + class_file: FileId, + class_id: TypeItemId, + arguments: &[(TypeId, TypeId)], ) -> QueryResult<()> where Q: ExternalQueries, { - let Some(class_info) = - constraint::lookup_file_class(state, context, input.class_file, input.class_id)? + let Some(class_info) = constraint::lookup_file_class(state, context, class_file, class_id)? else { return Ok(()); }; @@ -72,7 +66,7 @@ where let initial_level = class_info.quantified_variables.0 + class_info.kind_variables.0; let mut bindings = FxHashMap::default(); - for (index, &(argument_type, _)) in input.arguments.iter().enumerate() { + for (index, &(argument_type, _)) in arguments.iter().enumerate() { let level = debruijn::Level(initial_level + index as u32); bindings.insert(level, argument_type); } @@ -109,7 +103,8 @@ pub fn register_derived_instance( state: &mut CheckState, context: &CheckContext, input: ElaboratedDerive, -) where +) -> QueryResult<()> +where Q: ExternalQueries, { let ElaboratedDerive { derive_id, constraints, arguments, class_file, class_id } = input; @@ -124,13 +119,7 @@ pub fn register_derived_instance( quantify::quantify_instance(state, &mut instance); - let _ = constraint::validate_instance_rows( - state, - context, - class_file, - class_id, - &instance.arguments, - ); + constraint::validate_instance_rows(state, context, class_file, class_id, &instance.arguments)?; for (t, k) in instance.arguments.iter_mut() { *t = transfer::globalize(state, context, *t); @@ -143,6 +132,8 @@ pub fn register_derived_instance( } state.checked.derived.insert(derive_id, instance); + + Ok(()) } /// Looks up data constructors for a type, handling cross-file lookups. diff --git a/compiler-core/checking/src/algorithm/derive/traversable.rs b/compiler-core/checking/src/algorithm/derive/traversable.rs index 5d2f6ae6..795da22a 100644 --- a/compiler-core/checking/src/algorithm/derive/traversable.rs +++ b/compiler-core/checking/src/algorithm/derive/traversable.rs @@ -3,17 +3,17 @@ use building_types::QueryResult; use crate::ExternalQueries; -use crate::algorithm::derive::variance::{Variance, VarianceConfig, generate_variance_constraints}; -use crate::algorithm::derive::{self, tools}; +use crate::algorithm::derive::variance::{Variance, VarianceConfig}; +use crate::algorithm::derive::{self, DeriveStrategy, tools}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::error::ErrorKind; -/// Checks a derive instance for Traversable. +/// Checks a derive instance head for Traversable. pub fn check_derive_traversable( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -24,31 +24,28 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; let traversable = Some((input.class_file, input.class_id)); - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Single((Variance::Covariant, traversable)); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } -/// Checks a derive instance for Bitraversable. +/// Checks a derive instance head for Bitraversable. pub fn check_derive_bitraversable( state: &mut CheckState, context: &CheckContext, input: tools::ElaboratedDerive, -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -59,26 +56,23 @@ where expected: 1, actual: input.arguments.len(), }); - return Ok(()); + return Ok(None); }; let Some((data_file, data_id)) = derive::extract_type_constructor(state, derived_type) else { let type_message = state.render_local_type(context, derived_type); state.insert_error(ErrorKind::CannotDeriveForType { type_message }); - return Ok(()); + return Ok(None); }; // Bitraversable derivation emits Traversable constraints for wrapped parameters. let traversable = context.known_types.traversable; - tools::push_given_constraints(state, &input.constraints); - tools::emit_superclass_constraints(state, context, &input)?; - tools::register_derived_instance(state, context, input); + tools::register_derived_instance(state, context, input)?; let config = VarianceConfig::Pair( (Variance::Covariant, traversable), (Variance::Covariant, traversable), ); - generate_variance_constraints(state, context, data_file, data_id, derived_type, config)?; - tools::solve_and_report_constraints(state, context) + Ok(Some(DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config })) } diff --git a/compiler-core/checking/src/algorithm/derive/variance.rs b/compiler-core/checking/src/algorithm/derive/variance.rs index ffa036a5..01520344 100644 --- a/compiler-core/checking/src/algorithm/derive/variance.rs +++ b/compiler-core/checking/src/algorithm/derive/variance.rs @@ -77,6 +77,7 @@ impl DerivedSkolems { pub type ParameterConfig = (Variance, Option<(FileId, TypeItemId)>); /// Configuration for variance-aware derivation. +#[derive(Clone, Copy)] pub enum VarianceConfig { Single(ParameterConfig), Pair(ParameterConfig, ParameterConfig), diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 1ff2b391..747ad069 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -57,7 +57,11 @@ where let expected = toolkit::collect_given_constraints(state, expected); let inferred = toolkit::instantiate_constrained(state, inferred); unification::subtype_with_mode( - state, context, inferred, expected, ElaborationMode::No, + state, + context, + inferred, + expected, + ElaborationMode::No, )?; crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) @@ -70,14 +74,22 @@ where let expected = toolkit::collect_given_constraints(state, expected); let inferred = toolkit::instantiate_constrained(state, inferred); unification::subtype_with_mode( - state, context, inferred, expected, ElaborationMode::No, + state, + context, + inferred, + expected, + ElaborationMode::No, )?; crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) } else { let inferred = toolkit::instantiate_constrained(state, inferred); unification::subtype_with_mode( - state, context, inferred, expected, ElaborationMode::No, + state, + context, + inferred, + expected, + ElaborationMode::No, )?; crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) diff --git a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap index 2718b5e0..7239f1a9 100644 --- a/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking/157_derive_functor_insufficient_params/Main.snap @@ -35,16 +35,16 @@ error[CannotUnify]: Cannot unify 'Type' with 'Type -> Type' | 6 | derive instance Functor (Pair Int String) | ^~~~~~~~~~~~~~~~~ -error[CannotDeriveForType]: Cannot derive for type: Pair Int String - --> 6:1..6:42 - | -6 | derive instance Functor (Pair Int String) - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'Type' with 'Type -> Type' --> 9:25..9:29 | 9 | derive instance Functor Unit | ^~~~ +error[CannotDeriveForType]: Cannot derive for type: Pair Int String + --> 6:1..6:42 + | +6 | derive instance Functor (Pair Int String) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ error[CannotDeriveForType]: Cannot derive for type: Unit --> 9:1..9:29 | diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index 8e16d36e..e9377dfc 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -98,7 +98,7 @@ derive Eq1 (Rec :: Type -> Type) derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&0 :: Type)) --> 35:1..35:43 | 35 | derive instance Eq1 f => Eq1 (Compose f g) diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index 9de28c7c..d669c541 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -69,12 +69,12 @@ derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: T derive Ord1 (&0 :: Type -> Type) => Ord1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&0 :: Type)) --> 26:1..26:43 | 26 | derive instance Eq1 f => Eq1 (Compose f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) (~&2 :: Type)) +error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) (~&0 :: Type)) --> 27:1..27:45 | 27 | derive instance Ord1 f => Ord1 (Compose f g) diff --git a/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap b/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap index af6963ca..e103bb7b 100644 --- a/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap +++ b/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms diff --git a/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.purs b/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.purs new file mode 100644 index 00000000..b7b49d4b --- /dev/null +++ b/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Data.Eq (class Eq) + +data DurationComponent = Hours | Minutes | Seconds + +data Duration = Duration DurationComponent Int + +-- Eq Duration depends on Eq DurationComponent, which is derived later. +derive instance Eq Duration +derive instance Eq DurationComponent diff --git a/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.snap b/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.snap new file mode 100644 index 00000000..94f3ae4f --- /dev/null +++ b/tests-integration/fixtures/checking/299_derive_mutual_visibility_same_module/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Hours :: DurationComponent +Minutes :: DurationComponent +Seconds :: DurationComponent +Duration :: DurationComponent -> Int -> Duration + +Types +DurationComponent :: Type +Duration :: Type + +Data +DurationComponent + Quantified = :0 + Kind = :0 + +Duration + Quantified = :0 + Kind = :0 + + +Roles +DurationComponent = [] +Duration = [] + +Derived +derive Eq (Duration :: Type) +derive Eq (DurationComponent :: Type) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index ef37e477..1e06d908 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -611,3 +611,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_297_applied_function_type_decomposition_main() { run_test("297_applied_function_type_decomposition", "Main"); } #[rustfmt::skip] #[test] fn test_298_operator_alias_class_method_main() { run_test("298_operator_alias_class_method", "Main"); } + +#[rustfmt::skip] #[test] fn test_299_derive_mutual_visibility_same_module_main() { run_test("299_derive_mutual_visibility_same_module", "Main"); } From 5c9d9fa67f125533252a3d5b62c0301d6edb033e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 01:25:13 +0800 Subject: [PATCH 121/386] Implement proper shifting for class members in instance checking --- .../checking/src/algorithm/term_item.rs | 33 +++++++++++++++++++ .../300_instance_shift_variables/Main.purs | 9 +++++ .../300_instance_shift_variables/Main.snap | 26 +++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 70 insertions(+) create mode 100644 tests-integration/fixtures/checking/300_instance_shift_variables/Main.purs create mode 100644 tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 652d9a33..5ea6dbec 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -508,13 +508,43 @@ where state.constraints.push_given(local_constraint); } + // The bound_count is used to shift the levels of class member types which + // were originally bound with only the class' type variables in scope. + // + // Here is a quick example with `Functor` and `Vector`, we can observe + // immediately that without any intervention, `map` falls into a scope + // conflict where now there's two variables at level 1, `n` and `a`. + // + // For example, without shifting + // + // ```purescript + // newtype Vector :: Symbol -> Int -> Type -> Type + // newtype Vector s:0 n:1 a:2 = Vector (Array a) + // + // class Functor f:0 where + // map :: forall a:1 b:2. (a -> b) -> f a -> f b + // + // instance Functor (Vector s:0 n:1) where + // -- map :: forall f:0 a:1 b:2. Functor f => (a -> b) -> f a -> f b + // map f (Vector x) = Vector (map f x) + // ``` + // + // Finally, with shifting + // + // ```purescript + // instance Functor (Vector s:0 n:1) where + // -- map :: forall f:2 a:3 b:4. Functor f => (a -> b) -> f a -> f b + // map f (Vector x) = Vector (map f x) + // ``` let specialized_type = if let Some(class_member_type) = class_member_type { + let bound_count = kind_variables.len() + instance_bindings.len(); specialize_class_member( state, context, class_member_type, (class_file, class_id), instance_arguments, + bound_count as u32, )? } else { None @@ -597,6 +627,7 @@ fn specialize_class_member( class_member_type: TypeId, (class_file, class_id): (FileId, TypeItemId), instance_arguments: &[(TypeId, TypeId)], + bound_count: u32, ) -> QueryResult> where Q: ExternalQueries, @@ -617,6 +648,8 @@ where let arguments = arguments.collect_vec(); let kind_variables = class_info.quantified_variables.0 + class_info.kind_variables.0; + specialized = substitute::ShiftBound::on(state, specialized, bound_count); + let mut kind_variables = 0..kind_variables; let mut arguments = arguments.into_iter(); diff --git a/tests-integration/fixtures/checking/300_instance_shift_variables/Main.purs b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.purs new file mode 100644 index 00000000..41f0caab --- /dev/null +++ b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Functor (class Functor, map) + +newtype Wrap :: forall k. Type -> (k -> Type) -> k -> Type +newtype Wrap e w a = Wrap (w a) + +instance Functor w => Functor (Wrap e w) where + map f (Wrap x) = Wrap (map f x) diff --git a/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap new file mode 100644 index 00000000..626e969c --- /dev/null +++ b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrap :: + forall (k :: Type) (e :: Type) (w :: (k :: Type) -> Type) (a :: (k :: Type)). + (w :: (k :: Type) -> Type) (a :: (k :: Type)) -> + Wrap @(k :: Type) (e :: Type) (w :: (k :: Type) -> Type) (a :: (k :: Type)) + +Types +Wrap :: forall (k :: Type). Type -> ((k :: Type) -> Type) -> (k :: Type) -> Type + +Data +Wrap + Quantified = :0 + Kind = :1 + + +Roles +Wrap = [Phantom, Representational, Nominal] + +Instances +instance Functor (&1 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type) (&1 :: Type -> Type) :: Type -> Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 1e06d908..e7183bef 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -613,3 +613,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_298_operator_alias_class_method_main() { run_test("298_operator_alias_class_method", "Main"); } #[rustfmt::skip] #[test] fn test_299_derive_mutual_visibility_same_module_main() { run_test("299_derive_mutual_visibility_same_module", "Main"); } + +#[rustfmt::skip] #[test] fn test_300_instance_shift_variables_main() { run_test("300_instance_shift_variables", "Main"); } From 0db7c0e64e2bffe6a3b30c408d9f90d0790c4f70 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 01:36:51 +0800 Subject: [PATCH 122/386] Normalise spelling from en-us to en-uk --- .../constraint/compiler_solved/prim_coerce.rs | 2 +- .../checking/src/algorithm/derive.rs | 2 +- .../checking/src/algorithm/derive/tools.rs | 4 +- compiler-core/checking/src/algorithm/fold.rs | 4 +- compiler-core/checking/src/algorithm/state.rs | 2 +- .../checking/src/algorithm/substitute.rs | 2 +- .../checking/src/algorithm/term_item.rs | 44 +++++++++---------- .../checking/src/algorithm/toolkit.rs | 6 +-- tests-integration/tests/checking.rs | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index 56dd1595..965c0561 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -334,7 +334,7 @@ where // decompose_kind_for_coercion instantiates the variables into // skolem variables, then returns the first argument, which in - // this case is the already-skolemized `~k` + // this case is the already skolemised `~k` // // left_kind_applied := Maybe @~k // left_domain := ~k diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index c5428340..14501c74 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -476,7 +476,7 @@ where Ok(global_type.map(|global_type| transfer::localize(state, context, global_type))) } -/// Gets the inner type for a newtype, specialized with type arguments. +/// Gets the inner type for a newtype, specialised with type arguments. /// /// Newtypes have exactly one constructor with exactly one field. /// This function extracts that field type, substituting any type parameters. diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 46c13458..a3cf5726 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -72,8 +72,8 @@ where } for &(superclass, _) in class_info.superclasses.iter() { - let specialized = substitute::SubstituteBindings::on(state, &bindings, superclass); - state.constraints.push_wanted(specialized); + let specialised = substitute::SubstituteBindings::on(state, &bindings, superclass); + state.constraints.push_wanted(specialised); } Ok(()) diff --git a/compiler-core/checking/src/algorithm/fold.rs b/compiler-core/checking/src/algorithm/fold.rs index 82230846..6895577a 100644 --- a/compiler-core/checking/src/algorithm/fold.rs +++ b/compiler-core/checking/src/algorithm/fold.rs @@ -18,10 +18,10 @@ pub trait TypeFold { fn transform_binder(&mut self, _binder: &mut ForallBinder) {} } -/// Zonking normalizes a type by substituting solved unification variables. +/// Zonking normalises a type by substituting solved unification variables. /// /// Unlike [`CheckState::normalize_type`] which only follows unification -/// chains at the head, this recursively normalizes the entire type structure. +/// chains at the head, this recursively normalises the entire type structure. /// /// The simplicity of the [`TypeFold`] implementation is an artefact of how /// [`fold_type`] uses [`CheckState::normalize_type`] to inspect a type. diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 173f8174..f998154c 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1137,7 +1137,7 @@ impl CheckState { } impl CheckState { - /// Normalizes unification and bound type variables. + /// Normalises unification and bound type variables. /// /// This function also applies path compression to unification variables, /// where if a unification variable `?0` solves to `?1`, which solves to diff --git a/compiler-core/checking/src/algorithm/substitute.rs b/compiler-core/checking/src/algorithm/substitute.rs index d64c9323..f0853e6c 100644 --- a/compiler-core/checking/src/algorithm/substitute.rs +++ b/compiler-core/checking/src/algorithm/substitute.rs @@ -109,7 +109,7 @@ pub struct SubstituteBindings<'a> { impl SubstituteBindings<'_> { /// Substitutes bound and implicit variables using a level-based mapping. /// - /// This is used to specialize class superclasses with instance arguments. + /// This is used to specialise class superclasses with instance arguments. /// For example, when deriving `Traversable (Compose f g)`, the superclass /// `Functor t` becomes `Functor (Compose f g)` by binding `t`'s level to /// `Compose f g`. diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 5ea6dbec..612e28a2 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -441,7 +441,7 @@ pub struct CheckInstanceMemberGroup<'a> { kind_variables: &'a [TypeId], } -/// Checks an instance member group against its specialized class member type. +/// Checks an instance member group against its specialised class member type. /// /// This rule maintains the following invariants: /// - Check mode: `inferred_type <: signature_type` and `signature_type ~ specialised_type` @@ -536,9 +536,9 @@ where // -- map :: forall f:2 a:3 b:4. Functor f => (a -> b) -> f a -> f b // map f (Vector x) = Vector (map f x) // ``` - let specialized_type = if let Some(class_member_type) = class_member_type { + let specialised_type = if let Some(class_member_type) = class_member_type { let bound_count = kind_variables.len() + instance_bindings.len(); - specialize_class_member( + specialise_class_member( state, context, class_member_type, @@ -550,9 +550,9 @@ where None }; - // The specialized type may have constraints like `Show a => (a -> b) -> f a -> f b`. + // The specialised type may have constraints like `Show a => (a -> b) -> f a -> f b`. // We push `Show a` as a given and use the body `(a -> b) -> f a -> f b` for checking. - let specialized_type = specialized_type.map(|mut t| { + let specialised_type = specialised_type.map(|mut t| { while let normalized = state.normalize_type(t) && let Type::Constrained(constraint, constrained) = &state.storage[normalized] { @@ -568,10 +568,10 @@ where let (member_type, _) = kind::check_surface_kind(state, context, *signature_id, context.prim.t)?; - if let Some(specialized_type) = specialized_type { - let unified = unification::unify(state, context, member_type, specialized_type)?; + if let Some(specialised_type) = specialised_type { + let unified = unification::unify(state, context, member_type, specialised_type)?; if !unified { - let expected = state.render_local_type(context, specialized_type); + let expected = state.render_local_type(context, specialised_type); let actual = state.render_local_type(context, member_type); state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } @@ -580,16 +580,16 @@ where let signature = inspect::inspect_signature(state, context, member_type, &surface_bindings)?; equation::check_equations(state, context, *signature_id, signature, &member.equations)?; - } else if let Some(specialized_type) = specialized_type { + } else if let Some(specialised_type) = specialised_type { let inferred_type = state.fresh_unification_type(context); equation::infer_equations_core(state, context, inferred_type, &member.equations)?; - let origin = equation::ExhaustivenessOrigin::FromType(specialized_type); + let origin = equation::ExhaustivenessOrigin::FromType(specialised_type); equation::patterns(state, context, origin, &member.equations)?; - let matches = unification::subtype(state, context, inferred_type, specialized_type)?; + let matches = unification::subtype(state, context, inferred_type, specialised_type)?; if !matches { - let expected = state.render_local_type(context, specialized_type); + let expected = state.render_local_type(context, specialised_type); let actual = state.render_local_type(context, inferred_type); state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); } @@ -617,11 +617,11 @@ macro_rules! debug_assert_class_constraint { }; } -/// Specializes a class member type for a specific instance. +/// Specialises a class member type for a specific instance. /// /// Given a class member type like `forall a. Show a => a -> String`, /// and instance arguments like `Int`, this returns `Int -> String`. -fn specialize_class_member( +fn specialise_class_member( state: &mut CheckState, context: &CheckContext, class_member_type: TypeId, @@ -637,7 +637,7 @@ where return Ok(None); }; - let mut specialized = class_member_type; + let mut specialised = class_member_type; let arguments = instance_arguments.iter().map(|(t, k)| { let t = transfer::localize(state, context, *t); @@ -648,12 +648,12 @@ where let arguments = arguments.collect_vec(); let kind_variables = class_info.quantified_variables.0 + class_info.kind_variables.0; - specialized = substitute::ShiftBound::on(state, specialized, bound_count); + specialised = substitute::ShiftBound::on(state, specialised, bound_count); let mut kind_variables = 0..kind_variables; let mut arguments = arguments.into_iter(); - while let normalized = state.normalize_type(specialized) + while let normalized = state.normalize_type(specialised) && let Type::Forall(binder, inner) = &state.storage[normalized] { let binder_level = binder.level; @@ -670,14 +670,14 @@ where state.storage.intern(Type::Variable(skolem)) }; - specialized = substitute::SubstituteBound::on(state, binder_level, replacement, inner); + specialised = substitute::SubstituteBound::on(state, binder_level, replacement, inner); } - specialized = state.normalize_type(specialized); - if let Type::Constrained(constraint, constrained) = state.storage[specialized] { + specialised = state.normalize_type(specialised); + if let Type::Constrained(constraint, constrained) = state.storage[specialised] { debug_assert_class_constraint!(state, constraint, class_file, class_id); - specialized = constrained; + specialised = constrained; } - Ok(Some(specialized)) + Ok(Some(specialised)) } diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index fc944252..4031c756 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -188,7 +188,7 @@ pub fn instantiate_with_arguments( arguments: impl AsRef<[TypeId]>, ) -> (TypeId, usize) { let mut arguments_iter = arguments.as_ref().iter().copied(); - let mut skolemized = 0; + let mut skolemised = 0; safe_loop! { type_id = state.normalize_type(type_id); @@ -199,7 +199,7 @@ pub fn instantiate_with_arguments( let inner = *inner; let argument_type = arguments_iter.next().unwrap_or_else(|| { - skolemized += 1; + skolemised += 1; let skolem = Variable::Skolem(binder_level, binder_kind); state.storage.intern(Type::Variable(skolem)) }); @@ -211,5 +211,5 @@ pub fn instantiate_with_arguments( } } - (type_id, skolemized) + (type_id, skolemised) } diff --git a/tests-integration/tests/checking.rs b/tests-integration/tests/checking.rs index 648f6833..0f663fb8 100644 --- a/tests-integration/tests/checking.rs +++ b/tests-integration/tests/checking.rs @@ -331,7 +331,7 @@ fn test_subtype_mono_of_poly_fail() { // Create ∀a. a -> a let forall_a_to_a = make_forall_a_to_a(context, state); - // (Int -> Int) <: ∀a. (a -> a) should fail (RHS forall gets skolemized) + // (Int -> Int) <: ∀a. (a -> a) should fail (RHS forall gets skolemised) let int_to_int = state.function(context.prim.int, context.prim.int); let result = unification::subtype(state, context, int_to_int, forall_a_to_a).unwrap(); assert!(!result, "(Int -> Int) <: ∀a. (a -> a) should fail"); From 3743a002348f6cb89eaca32952d195b8af86cf33 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 02:37:44 +0800 Subject: [PATCH 123/386] Elaborate symmetric constraints for Coercible, add rule for function coercion --- .../checking/src/algorithm/constraint.rs | 24 +++++++- .../constraint/compiler_solved/prim_coerce.rs | 30 ++++++++++ .../checking/src/algorithm/operator.rs | 55 +++-------------- .../checking/src/algorithm/toolkit.rs | 60 ++++++++++++++++++- .../checking/301_coercible_symmetry/Main.purs | 10 ++++ .../checking/301_coercible_symmetry/Main.snap | 23 +++++++ .../Main.purs | 29 +++++++++ .../Main.snap | 46 ++++++++++++++ .../checking/prelude/Safe.Coerce.purs | 2 +- tests-integration/tests/checking/generated.rs | 4 ++ 10 files changed, 232 insertions(+), 51 deletions(-) create mode 100644 tests-integration/fixtures/checking/301_coercible_symmetry/Main.purs create mode 100644 tests-integration/fixtures/checking/301_coercible_symmetry/Main.snap create mode 100644 tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.purs create mode 100644 tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.snap diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 2ef6bb40..822e3e5f 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -156,7 +156,29 @@ where elaborate_superclasses(state, context, constraint, &mut elaborated)?; } - Ok(elaborated.into_iter().filter_map(|given| constraint_application(state, given)).collect()) + let applications = + elaborated.into_iter().filter_map(|given| constraint_application(state, given)); + let mut applications = applications.collect_vec(); + + let is_coercible = |file_id, item_id| { + (&context.prim_coerce).file_id == file_id && (&context.prim_coerce).coercible == item_id + }; + + // For coercible applications, also elaborate into symmetric versions. + let symmetric = applications.iter().filter_map(|application| { + let is_coercible = is_coercible(application.file_id, application.item_id); + let &[left, right] = application.arguments.as_slice() else { return None }; + is_coercible.then(|| ConstraintApplication { + file_id: application.file_id, + item_id: application.item_id, + arguments: vec![right, left], + }) + }); + + let reversed = symmetric.collect_vec(); + applications.extend(reversed); + + Ok(applications) } /// Discovers superclass constraints for a given constraint. diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index 965c0561..9455b89a 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -50,6 +50,10 @@ where return Ok(Some(result)); } + if let Some(result) = try_function_coercion(state, context, left, right)? { + return Ok(Some(result)); + } + if let Some(result) = try_higher_kinded_coercion(state, context, left, right)? { return Ok(Some(result)); } @@ -258,6 +262,32 @@ where } } +/// Decomposes `Coercible (a -> b) (c -> d)` into `Coercible a c` and `Coercible b d`. +fn try_function_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let strict = toolkit::SynthesiseFunction::No; + let Some((left_argument, left_result)) = + toolkit::decompose_function(state, context, left, strict)? + else { + return Ok(None); + }; + let Some((right_argument, right_result)) = + toolkit::decompose_function(state, context, right, strict)? + else { + return Ok(None); + }; + let c1 = make_coercible_constraint(state, context, left_argument, right_argument); + let c2 = make_coercible_constraint(state, context, left_result, right_result); + Ok(Some(MatchInstance::Match { constraints: vec![c1, c2], equalities: vec![] })) +} + fn try_row_coercion( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking/src/algorithm/operator.rs b/compiler-core/checking/src/algorithm/operator.rs index 1e6dfd95..f9faa40f 100644 --- a/compiler-core/checking/src/algorithm/operator.rs +++ b/compiler-core/checking/src/algorithm/operator.rs @@ -114,12 +114,17 @@ where let operator_type = toolkit::instantiate_forall(state, operator_type); let operator_type = toolkit::collect_constraints(state, operator_type); - let Some((left_type, operator_type)) = decompose_function(state, context, operator_type)? + let synthesise = toolkit::SynthesiseFunction::Yes; + + let Some((left_type, operator_type)) = + toolkit::decompose_function(state, context, operator_type, synthesise)? else { return Ok(unknown); }; - let Some((right_type, result_type)) = decompose_function(state, context, operator_type)? else { + let Some((right_type, result_type)) = + toolkit::decompose_function(state, context, operator_type, synthesise)? + else { return Ok(unknown); }; @@ -151,52 +156,6 @@ where Ok(E::build(state, context, operator, (left, right), result_type)) } -/// Decompose a type into `(argument, result)` as if it were a function type. -/// -/// This function handles three representations: -/// - `Type::Function(a, b)`, the standard function representation -/// - `Type::Application(Application(f, a), b)`, the application-based form -/// - `Type::Unification(u)`, which requires function type synthesis -fn decompose_function( - state: &mut CheckState, - context: &CheckContext, - t: TypeId, -) -> QueryResult> -where - Q: ExternalQueries, -{ - match state.storage[t] { - Type::Function(argument, result) => Ok(Some((argument, result))), - - Type::Unification(unification_id) => { - let argument = state.fresh_unification_type(context); - let result = state.fresh_unification_type(context); - - let function = state.storage.intern(Type::Function(argument, result)); - state.unification.solve(unification_id, function); - - Ok(Some((argument, result))) - } - - Type::Application(partial, result) => { - let partial = state.normalize_type(partial); - if let Type::Application(constructor, argument) = state.storage[partial] { - let constructor = state.normalize_type(constructor); - if constructor == context.prim.function { - return Ok(Some((argument, result))); - } - if let Type::Unification(unification_id) = state.storage[constructor] { - state.unification.solve(unification_id, context.prim.function); - return Ok(Some((argument, result))); - } - } - Ok(None) - } - - _ => Ok(None), - } -} - pub trait IsOperator: IsElement { type ItemId: Copy; type Elaborated: Copy; diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 4031c756..c2080422 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -1,5 +1,8 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; -use crate::algorithm::state::CheckState; +use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::substitute; use crate::core::{Type, TypeId, Variable}; @@ -213,3 +216,58 @@ pub fn instantiate_with_arguments( (type_id, skolemised) } + +#[derive(Copy, Clone, Debug)] +pub enum SynthesiseFunction { + Yes, + No, +} + +/// Decompose a type into `(argument, result)` as if it were a function type. +/// +/// Handles three representations: +/// - `Type::Function(a, b)`, the standard function representation +/// - `Type::Application(Application(f, a), b)`, the application-based form +/// - `Type::Unification(u)`, which requires function type synthesis +pub fn decompose_function( + state: &mut CheckState, + context: &CheckContext, + t: TypeId, + mode: SynthesiseFunction, +) -> QueryResult> +where + Q: ExternalQueries, +{ + match state.storage[t] { + Type::Function(argument, result) => Ok(Some((argument, result))), + + Type::Unification(unification_id) if matches!(mode, SynthesiseFunction::Yes) => { + let argument = state.fresh_unification_type(context); + let result = state.fresh_unification_type(context); + + let function = state.storage.intern(Type::Function(argument, result)); + state.unification.solve(unification_id, function); + + Ok(Some((argument, result))) + } + + Type::Application(partial, result) => { + let partial = state.normalize_type(partial); + if let Type::Application(constructor, argument) = state.storage[partial] { + let constructor = state.normalize_type(constructor); + if constructor == context.prim.function { + return Ok(Some((argument, result))); + } + if matches!(mode, SynthesiseFunction::Yes) + && let Type::Unification(unification_id) = state.storage[constructor] + { + state.unification.solve(unification_id, context.prim.function); + return Ok(Some((argument, result))); + } + } + Ok(None) + } + + _ => Ok(None), + } +} diff --git a/tests-integration/fixtures/checking/301_coercible_symmetry/Main.purs b/tests-integration/fixtures/checking/301_coercible_symmetry/Main.purs new file mode 100644 index 00000000..f197ac85 --- /dev/null +++ b/tests-integration/fixtures/checking/301_coercible_symmetry/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Newtype (class Newtype, wrap) + +newtype Age = Age Int + +derive instance Newtype Age _ + +wrapAge :: Int -> Age +wrapAge = wrap diff --git a/tests-integration/fixtures/checking/301_coercible_symmetry/Main.snap b/tests-integration/fixtures/checking/301_coercible_symmetry/Main.snap new file mode 100644 index 00000000..fe88ee23 --- /dev/null +++ b/tests-integration/fixtures/checking/301_coercible_symmetry/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +wrapAge :: Int -> Age + +Types +Age :: Type + +Data +Age + Quantified = :0 + Kind = :0 + + +Roles +Age = [] + +Derived +derive Newtype (Age :: Type) (Int :: Type) diff --git a/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.purs b/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.purs new file mode 100644 index 00000000..e34a355c --- /dev/null +++ b/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.purs @@ -0,0 +1,29 @@ +module Main where + +import Safe.Coerce (class Coercible, coerce) +import Data.Newtype (class Newtype) + +newtype Age = Age Int + +derive instance Newtype Age _ + +coerceFn :: (Age -> Int) -> (Int -> Age) +coerceFn = coerce + +over :: forall t a s b. Newtype t a => Newtype s b => (a -> t) -> (a -> b) -> t -> s +over _ = coerce + +under :: forall t a s b. Newtype t a => Newtype s b => (a -> t) -> (t -> s) -> a -> b +under _ = coerce + +alaF + :: forall f g t a s b + . Coercible (f t) (f a) + => Coercible (g s) (g b) + => Newtype t a + => Newtype s b + => (a -> t) + -> (f t -> g s) + -> f a + -> g b +alaF _ = coerce diff --git a/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.snap b/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.snap new file mode 100644 index 00000000..663869e5 --- /dev/null +++ b/tests-integration/fixtures/checking/302_coercible_function_decomposition/Main.snap @@ -0,0 +1,46 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +coerceFn :: (Age -> Int) -> Int -> Age +over :: + forall (t :: Type) (a :: Type) (s :: Type) (b :: Type). + Newtype @Type (t :: Type) (a :: Type) => + Newtype @Type (s :: Type) (b :: Type) => + ((a :: Type) -> (t :: Type)) -> ((a :: Type) -> (b :: Type)) -> (t :: Type) -> (s :: Type) +under :: + forall (t :: Type) (a :: Type) (s :: Type) (b :: Type). + Newtype @Type (t :: Type) (a :: Type) => + Newtype @Type (s :: Type) (b :: Type) => + ((a :: Type) -> (t :: Type)) -> ((t :: Type) -> (s :: Type)) -> (a :: Type) -> (b :: Type) +alaF :: + forall (t37 :: Type) (f :: Type -> Type) (g :: (t37 :: Type) -> Type) (t :: Type) (a :: Type) + (s :: (t37 :: Type)) (b :: (t37 :: Type)). + Coercible @Type ((f :: Type -> Type) (t :: Type)) ((f :: Type -> Type) (a :: Type)) => + Coercible @Type + ((g :: (t37 :: Type) -> Type) (s :: (t37 :: Type))) + ((g :: (t37 :: Type) -> Type) (b :: (t37 :: Type))) => + Newtype @Type (t :: Type) (a :: Type) => + Newtype @(t37 :: Type) (s :: (t37 :: Type)) (b :: (t37 :: Type)) => + ((a :: Type) -> (t :: Type)) -> + ((f :: Type -> Type) (t :: Type) -> (g :: (t37 :: Type) -> Type) (s :: (t37 :: Type))) -> + (f :: Type -> Type) (a :: Type) -> + (g :: (t37 :: Type) -> Type) (b :: (t37 :: Type)) + +Types +Age :: Type + +Data +Age + Quantified = :0 + Kind = :0 + + +Roles +Age = [] + +Derived +derive Newtype (Age :: Type) (Int :: Type) diff --git a/tests-integration/fixtures/checking/prelude/Safe.Coerce.purs b/tests-integration/fixtures/checking/prelude/Safe.Coerce.purs index d701d143..c24a3e05 100644 --- a/tests-integration/fixtures/checking/prelude/Safe.Coerce.purs +++ b/tests-integration/fixtures/checking/prelude/Safe.Coerce.purs @@ -1,4 +1,4 @@ -module Safe.Coerce where +module Safe.Coerce (coerce, module Prim.Coerce) where import Prim.Coerce (class Coercible) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index e7183bef..1d38e53e 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -615,3 +615,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_299_derive_mutual_visibility_same_module_main() { run_test("299_derive_mutual_visibility_same_module", "Main"); } #[rustfmt::skip] #[test] fn test_300_instance_shift_variables_main() { run_test("300_instance_shift_variables", "Main"); } + +#[rustfmt::skip] #[test] fn test_301_coercible_symmetry_main() { run_test("301_coercible_symmetry", "Main"); } + +#[rustfmt::skip] #[test] fn test_302_coercible_function_decomposition_main() { run_test("302_coercible_function_decomposition", "Main"); } From c39bcb82fcb658e3f6b1fa33b786b6e2d99f5b79 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 03:01:32 +0800 Subject: [PATCH 124/386] Remove unused code in compiler-compatibility --- compiler-compatibility/src/compat.rs | 8 +------- compiler-compatibility/src/layout.rs | 7 +------ compiler-compatibility/src/registry.rs | 4 ---- compiler-compatibility/src/types.rs | 27 -------------------------- 4 files changed, 2 insertions(+), 44 deletions(-) diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/src/compat.rs index 8f65b3ef..6c91d1d0 100644 --- a/compiler-compatibility/src/compat.rs +++ b/compiler-compatibility/src/compat.rs @@ -14,7 +14,6 @@ use crate::loader; /// Result of checking a single file. pub struct FileResult { - pub relative_path: String, pub error_count: usize, pub warning_count: usize, pub output: String, @@ -96,7 +95,6 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & let Ok((parsed, _)) = engine.parsed(id) else { return FileResult { - relative_path: relative_path.to_string(), error_count: 1, warning_count: 0, output: format!("{relative_path}:1:1: error[ParseError]: Failed to parse file\n"), @@ -114,7 +112,6 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & Ok(s) => s, Err(_) => { return FileResult { - relative_path: relative_path.to_string(), error_count: 1, warning_count: 0, output: format!( @@ -128,7 +125,6 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & Ok(i) => i, Err(_) => { return FileResult { - relative_path: relative_path.to_string(), error_count: 1, warning_count: 0, output: format!("{relative_path}:1:1: error[IndexError]: Failed to index\n"), @@ -144,7 +140,6 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & Ok(l) => l, Err(_) => { return FileResult { - relative_path: relative_path.to_string(), error_count: 1, warning_count: 0, output: format!("{relative_path}:1:1: error[LowerError]: Failed to lower\n"), @@ -156,7 +151,6 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & Ok(c) => c, Err(_) => { return FileResult { - relative_path: relative_path.to_string(), error_count: 1, warning_count: 0, output: format!("{relative_path}:1:1: error[CheckError]: Failed to check\n"), @@ -212,7 +206,7 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & )); } - FileResult { relative_path: relative_path.to_string(), error_count, warning_count, output } + FileResult { error_count, warning_count, output } } fn offset_to_line_col(line_index: &LineIndex, content: &str, offset: TextSize) -> (u32, u32) { diff --git a/compiler-compatibility/src/layout.rs b/compiler-compatibility/src/layout.rs index 0807f286..067e5df7 100644 --- a/compiler-compatibility/src/layout.rs +++ b/compiler-compatibility/src/layout.rs @@ -7,7 +7,6 @@ use purescript_registry::RegistryLayout; /// Includes paths for repository checkouts, tarball cache, and unpacked packages. #[derive(Debug, Clone)] pub struct Layout { - pub root: PathBuf, pub repos_dir: PathBuf, pub registry_dir: PathBuf, pub index_dir: PathBuf, @@ -24,7 +23,7 @@ impl Layout { let cache_tarballs_dir = root.join("cache").join("tarballs"); let packages_dir = root.join("packages"); - Layout { root, repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } + Layout { repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } } pub fn registry_layout(&self) -> RegistryLayout { @@ -34,8 +33,4 @@ impl Layout { pub fn tarball_cache_path(&self, name: &str, version: &str) -> PathBuf { self.cache_tarballs_dir.join(format!("{}-{}.tar.gz", name, version)) } - - pub fn package_dir(&self, name: &str, version: &str) -> PathBuf { - self.packages_dir.join(format!("{}-{}", name, version)) - } } diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs index 4f332965..01f72e11 100644 --- a/compiler-compatibility/src/registry.rs +++ b/compiler-compatibility/src/registry.rs @@ -59,10 +59,6 @@ impl Registry { Ok(()) } - pub fn layout(&self) -> &Layout { - &self.layout - } - pub fn reader(&self) -> &FsRegistry { &self.reader } diff --git a/compiler-compatibility/src/types.rs b/compiler-compatibility/src/types.rs index 6140dd08..9c300652 100644 --- a/compiler-compatibility/src/types.rs +++ b/compiler-compatibility/src/types.rs @@ -1,31 +1,4 @@ -use std::path::PathBuf; - -#[derive(Debug, Clone)] -pub struct ResolvedPackage { - pub name: String, - pub version: String, -} - #[derive(Debug, Clone, Default)] pub struct ResolvedSet { pub packages: std::collections::BTreeMap, } - -#[derive(Debug, Clone)] -pub struct CompatConfig { - pub root: PathBuf, - pub no_cache: bool, - pub update_repos: bool, - pub package_set: Option, -} - -impl Default for CompatConfig { - fn default() -> CompatConfig { - CompatConfig { - root: PathBuf::from("target/compiler-compatibility"), - no_cache: false, - update_repos: false, - package_set: None, - } - } -} From 6d2297ba90fc238db3ae908cb93ab7d1642bac3b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 03:18:11 +0800 Subject: [PATCH 125/386] Implement proper shifting for synonym expansion Similar to class members 5c9d9fa67f12, synonym bodies need to be shifted to align with the current scope. We use the current size of the scope as an offset because synonym bodies are originally bound starting at 0. This commit also updates class member shifting to use the size of the current scope for symmetry. --- .../checking/src/algorithm/kind/synonym.rs | 25 +++++++++ .../checking/src/algorithm/term_item.rs | 54 ++++++++----------- .../303_instance_given_constraint/Main.purs | 24 +++++++++ .../303_instance_given_constraint/Main.snap | 51 ++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 tests-integration/fixtures/checking/303_instance_given_constraint/Main.purs create mode 100644 tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 33156c4f..341896d5 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -16,6 +16,7 @@ use lowering::{GroupedModule, LoweredModule, TypeItemIr}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, substitute, transfer, unification}; +use crate::core::debruijn; use crate::core::{Saturation, Synonym, Type, TypeId}; use crate::error::ErrorKind; use crate::{CheckedModule, ExternalQueries}; @@ -469,6 +470,30 @@ fn instantiate_saturated(state: &mut CheckState, synonym: Synonym, arguments: &[ let count = synonym.quantified_variables.0 as usize + synonym.kind_variables.0 as usize; let mut instantiated = state.normalize_type(synonym.synonym_type); + // Synonym bodies are originally bound starting at level 0. When expanding + // in a non-empty scope, we shift their levels up by the current scope size + // size to avoid conflicts. + // + // First, without shifting + // + // ```purescript + // type Transform f:0 g:1 = forall a:2. f a -> g a + // + // instance Parallel (ReaderT e:0 f:1) (ReaderT e:0 m:2) where + // -- parallel :: m:2 ~> f:1 + // -- :: forall a:2. m a -> f a + // ``` + // + // Then, with shifting + // + // ```purescript + // instance Parallel (ReaderT e:0 f:1) (ReaderT e:0 m:2) where + // -- parallel :: m:2 ~> f:1 + // -- :: forall a:5. m a -> f a + // ``` + let debruijn::Size(scope_size) = state.type_scope.size(); + instantiated = substitute::ShiftBound::on(state, instantiated, scope_size); + for _ in 0..count { if let Type::Forall(ref binder, inner) = state.storage[instantiated] { let binder_level = binder.level; diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 612e28a2..95f8b6ed 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -508,43 +508,13 @@ where state.constraints.push_given(local_constraint); } - // The bound_count is used to shift the levels of class member types which - // were originally bound with only the class' type variables in scope. - // - // Here is a quick example with `Functor` and `Vector`, we can observe - // immediately that without any intervention, `map` falls into a scope - // conflict where now there's two variables at level 1, `n` and `a`. - // - // For example, without shifting - // - // ```purescript - // newtype Vector :: Symbol -> Int -> Type -> Type - // newtype Vector s:0 n:1 a:2 = Vector (Array a) - // - // class Functor f:0 where - // map :: forall a:1 b:2. (a -> b) -> f a -> f b - // - // instance Functor (Vector s:0 n:1) where - // -- map :: forall f:0 a:1 b:2. Functor f => (a -> b) -> f a -> f b - // map f (Vector x) = Vector (map f x) - // ``` - // - // Finally, with shifting - // - // ```purescript - // instance Functor (Vector s:0 n:1) where - // -- map :: forall f:2 a:3 b:4. Functor f => (a -> b) -> f a -> f b - // map f (Vector x) = Vector (map f x) - // ``` let specialised_type = if let Some(class_member_type) = class_member_type { - let bound_count = kind_variables.len() + instance_bindings.len(); specialise_class_member( state, context, class_member_type, (class_file, class_id), instance_arguments, - bound_count as u32, )? } else { None @@ -627,7 +597,6 @@ fn specialise_class_member( class_member_type: TypeId, (class_file, class_id): (FileId, TypeItemId), instance_arguments: &[(TypeId, TypeId)], - bound_count: u32, ) -> QueryResult> where Q: ExternalQueries, @@ -648,7 +617,28 @@ where let arguments = arguments.collect_vec(); let kind_variables = class_info.quantified_variables.0 + class_info.kind_variables.0; - specialised = substitute::ShiftBound::on(state, specialised, bound_count); + // Class member types are originally bound with only the class' type + // variables in scope. When specialising for an instance, we shift + // their levels up by the current scope size to avoid conflicts. + // + // First, without shifting + // + // ```purescript + // class Functor f:0 where + // map :: forall a:1 b:2. (a -> b) -> f a -> f b + // + // instance Functor (Vector s:0 n:1) where + // -- map :: forall f:0 a:1 b:2. (a -> b) -> f a -> f b + // ``` + // + // Then, with shifting + // + // ```purescript + // instance Functor (Vector s:0 n:1) where + // -- map :: forall f:2 a:3 b:4. (a -> b) -> f a -> f b + // ``` + let debruijn::Size(scope_size) = state.type_scope.size(); + specialised = substitute::ShiftBound::on(state, specialised, scope_size); let mut kind_variables = 0..kind_variables; let mut arguments = arguments.into_iter(); diff --git a/tests-integration/fixtures/checking/303_instance_given_constraint/Main.purs b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.purs new file mode 100644 index 00000000..0a414e64 --- /dev/null +++ b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Data.Functor (class Functor) + +-- Class members using a type synonym with forall, combined with +-- fundeps and given constraints from the instance context. + +type Transform :: (Type -> Type) -> (Type -> Type) -> Type +type Transform f g = forall a. f a -> g a + +infixr 4 type Transform as ~> + +class (Functor m, Functor f) <= Parallel (f :: Type -> Type) (m :: Type -> Type) | m -> f, f -> m where + parallel :: m ~> f + sequential :: f ~> m + +newtype ReaderT r (m :: Type -> Type) a = ReaderT (r -> m a) + +mapReaderT :: forall r m n a b. (m a -> n b) -> ReaderT r m a -> ReaderT r n b +mapReaderT f (ReaderT g) = ReaderT (\r -> f (g r)) + +instance (Parallel f m) => Parallel (ReaderT e f) (ReaderT e m) where + parallel = mapReaderT parallel + sequential = mapReaderT sequential diff --git a/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap new file mode 100644 index 00000000..aaebe528 --- /dev/null +++ b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap @@ -0,0 +1,51 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +parallel :: + forall (f :: Type -> Type) (m :: Type -> Type). + Parallel (f :: Type -> Type) (m :: Type -> Type) => (m :: Type -> Type) ~> (f :: Type -> Type) +sequential :: + forall (f :: Type -> Type) (m :: Type -> Type). + Parallel (f :: Type -> Type) (m :: Type -> Type) => (f :: Type -> Type) ~> (m :: Type -> Type) +ReaderT :: + forall (r :: Type) (m :: Type -> Type) (a :: Type). + ((r :: Type) -> (m :: Type -> Type) (a :: Type)) -> + ReaderT (r :: Type) (m :: Type -> Type) (a :: Type) +mapReaderT :: + forall (r :: Type) (m :: Type -> Type) (n :: Type -> Type) (a :: Type) (b :: Type). + ((m :: Type -> Type) (a :: Type) -> (n :: Type -> Type) (b :: Type)) -> + ReaderT (r :: Type) (m :: Type -> Type) (a :: Type) -> + ReaderT (r :: Type) (n :: Type -> Type) (b :: Type) + +Types +Transform :: (Type -> Type) -> (Type -> Type) -> Type +~> :: (Type -> Type) -> (Type -> Type) -> Type +Parallel :: (Type -> Type) -> (Type -> Type) -> Constraint +ReaderT :: Type -> (Type -> Type) -> Type -> Type + +Synonyms +Transform = forall (f :: Type -> Type) (g :: Type -> Type) (a :: Type). + (f :: Type -> Type) (a :: Type) -> (g :: Type -> Type) (a :: Type) + Quantified = :0 + Kind = :0 + Type = :2 + + +Data +ReaderT + Quantified = :0 + Kind = :0 + + +Roles +ReaderT = [Representational, Representational, Nominal] + +Classes +class Functor (&1 :: Type -> Type), Functor (&0 :: Type -> Type) <= Parallel (&0 :: Type -> Type) (&1 :: Type -> Type) + +Instances +instance Parallel (&1 :: Type -> Type) (&2 :: Type -> Type) => Parallel (ReaderT (&0 :: Type) (&1 :: Type -> Type) :: Type -> Type) (ReaderT (&0 :: Type) (&2 :: Type -> Type) :: Type -> Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 1d38e53e..c6c5fd23 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -619,3 +619,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_301_coercible_symmetry_main() { run_test("301_coercible_symmetry", "Main"); } #[rustfmt::skip] #[test] fn test_302_coercible_function_decomposition_main() { run_test("302_coercible_function_decomposition", "Main"); } + +#[rustfmt::skip] #[test] fn test_303_instance_given_constraint_main() { run_test("303_instance_given_constraint", "Main"); } From 5f435189429182c86a11fa480503fc002a6b737a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 05:05:22 +0800 Subject: [PATCH 126/386] Normalise operator applications to constructor application form --- .../checking/src/algorithm/kind/synonym.rs | 39 ++++++++++++++++++- .../305_type_operator_unification/Main.purs | 11 ++++++ .../305_type_operator_unification/Main.snap | 25 ++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/305_type_operator_unification/Main.purs create mode 100644 tests-integration/fixtures/checking/305_type_operator_unification/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 341896d5..3d99e034 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -542,6 +542,42 @@ where }) } +/// Normalises a type operator application to constructor application form. +fn expand_type_operator( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Type::OperatorApplication(file_id, item_id, left, right) = state.storage[type_id] else { + return Ok(type_id); + }; + + let resolution = if file_id == context.id { + context.lowered.info.get_type_item(item_id).and_then(|ir| match ir { + TypeItemIr::Operator { resolution, .. } => *resolution, + _ => None, + }) + } else { + context.queries.lowered(file_id)?.info.get_type_item(item_id).and_then(|ir| match ir { + TypeItemIr::Operator { resolution, .. } => *resolution, + _ => None, + }) + }; + + let Some((file_id, item_id)) = resolution else { + return Ok(type_id); + }; + + let constructor = state.storage.intern(Type::Constructor(file_id, item_id)); + let left = state.storage.intern(Type::Application(constructor, left)); + let right = state.storage.intern(Type::Application(left, right)); + + Ok(right) +} + pub fn normalize_expand_type( state: &mut CheckState, context: &CheckContext, @@ -554,7 +590,8 @@ where for _ in 0..EXPANSION_LIMIT { let normalized_id = state.normalize_type(type_id); - let expanded_id = expand_type_synonym(state, context, normalized_id)?; + let expanded_id = expand_type_operator(state, context, normalized_id)?; + let expanded_id = expand_type_synonym(state, context, expanded_id)?; if expanded_id == type_id { return Ok(type_id); diff --git a/tests-integration/fixtures/checking/305_type_operator_unification/Main.purs b/tests-integration/fixtures/checking/305_type_operator_unification/Main.purs new file mode 100644 index 00000000..2b112ff8 --- /dev/null +++ b/tests-integration/fixtures/checking/305_type_operator_unification/Main.purs @@ -0,0 +1,11 @@ +module Main where + +data Either a b = Left a | Right b + +infixr 6 type Either as \/ + +in1 :: forall a z. a -> a \/ z +in1 = Left + +in2 :: forall a b z. b -> a \/ b \/ z +in2 v = Right (Left v) diff --git a/tests-integration/fixtures/checking/305_type_operator_unification/Main.snap b/tests-integration/fixtures/checking/305_type_operator_unification/Main.snap new file mode 100644 index 00000000..54fe134d --- /dev/null +++ b/tests-integration/fixtures/checking/305_type_operator_unification/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +in1 :: forall (a :: Type) (z :: Type). (a :: Type) -> (a :: Type) \/ (z :: Type) +in2 :: + forall (a :: Type) (b :: Type) (z :: Type). + (b :: Type) -> (a :: Type) \/ (b :: Type) \/ (z :: Type) + +Types +Either :: Type -> Type -> Type +\/ :: Type -> Type -> Type + +Data +Either + Quantified = :0 + Kind = :0 + + +Roles +Either = [Representational, Representational] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index c6c5fd23..503754a1 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -621,3 +621,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_302_coercible_function_decomposition_main() { run_test("302_coercible_function_decomposition", "Main"); } #[rustfmt::skip] #[test] fn test_303_instance_given_constraint_main() { run_test("303_instance_given_constraint", "Main"); } + +#[rustfmt::skip] #[test] fn test_305_type_operator_unification_main() { run_test("305_type_operator_unification", "Main"); } From 643c4d8aa4e24683dde05f3f06df8214e89ad95f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 05:18:41 +0800 Subject: [PATCH 127/386] Improve handling for Application-based Function types This commit adds missing cases in match_type, match_given_type, and can_unify. It also changes the approach from pattern matching deeply against the Type::Application, to reconstructing a Type::Application from the Type::Function to unify against. --- .../checking/src/algorithm/constraint.rs | 64 ++++++++++++--- .../checking/src/algorithm/unification.rs | 79 +++++-------------- .../Main.purs | 11 +++ .../Main.snap | 27 +++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 110 insertions(+), 73 deletions(-) create mode 100644 tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.purs create mode 100644 tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 822e3e5f..96a874c6 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -542,6 +542,18 @@ where .and_also(|| match_type(state, context, bindings, equalities, w_result, g_result)) } + (&Type::Function(w_argument, w_result), &Type::Application(_, _)) => { + let wanted = state.storage.intern(Type::Application(context.prim.function, w_argument)); + let wanted = state.storage.intern(Type::Application(wanted, w_result)); + match_type(state, context, bindings, equalities, wanted, given) + } + + (&Type::Application(_, _), &Type::Function(g_argument, g_result)) => { + let given = state.storage.intern(Type::Application(context.prim.function, g_argument)); + let given = state.storage.intern(Type::Application(given, g_result)); + match_type(state, context, bindings, equalities, wanted, given) + } + ( &Type::KindApplication(w_function, w_argument), &Type::KindApplication(g_function, g_argument), @@ -681,7 +693,15 @@ where /// found in value signatures rather than top-level instance declarations; /// unlike [`match_type`], this function does not build bindings or equalities /// for [`Variable::Bound`] or [`Variable::Implicit`] variables. -fn match_given_type(state: &mut CheckState, wanted: TypeId, given: TypeId) -> MatchType { +fn match_given_type( + state: &mut CheckState, + context: &CheckContext, + wanted: TypeId, + given: TypeId, +) -> MatchType +where + Q: ExternalQueries, +{ let wanted = state.normalize_type(wanted); let given = state.normalize_type(given); @@ -700,7 +720,7 @@ fn match_given_type(state: &mut CheckState, wanted: TypeId, given: TypeId) -> Ma Type::Variable(Variable::Bound(g_level, g_kind)), ) => { if w_level == g_level { - match_given_type(state, *w_kind, *g_kind) + match_given_type(state, context, *w_kind, *g_kind) } else { MatchType::Apart } @@ -711,7 +731,7 @@ fn match_given_type(state: &mut CheckState, wanted: TypeId, given: TypeId) -> Ma Type::Variable(Variable::Skolem(g_level, g_kind)), ) => { if w_level == g_level { - match_given_type(state, *w_kind, *g_kind) + match_given_type(state, context, *w_kind, *g_kind) } else { MatchType::Apart } @@ -720,26 +740,39 @@ fn match_given_type(state: &mut CheckState, wanted: TypeId, given: TypeId) -> Ma ( &Type::Application(w_function, w_argument), &Type::Application(g_function, g_argument), - ) => match_given_type(state, w_function, g_function) - .and_also(|| match_given_type(state, w_argument, g_argument)), + ) => match_given_type(state, context, w_function, g_function) + .and_also(|| match_given_type(state, context, w_argument, g_argument)), (&Type::Function(w_argument, w_result), &Type::Function(g_argument, g_result)) => { - match_given_type(state, w_argument, g_argument) - .and_also(|| match_given_type(state, w_result, g_result)) + match_given_type(state, context, w_argument, g_argument) + .and_also(|| match_given_type(state, context, w_result, g_result)) + } + + (&Type::Function(w_argument, w_result), &Type::Application(_, _)) => { + let wanted = state.storage.intern(Type::Application(context.prim.function, w_argument)); + let wanted = state.storage.intern(Type::Application(wanted, w_result)); + match_given_type(state, context, wanted, given) + } + + (&Type::Application(_, _), &Type::Function(g_argument, g_result)) => { + let given = state.storage.intern(Type::Application(context.prim.function, g_argument)); + let given = state.storage.intern(Type::Application(given, g_result)); + match_given_type(state, context, wanted, given) } ( &Type::KindApplication(w_function, w_argument), &Type::KindApplication(g_function, g_argument), - ) => match_given_type(state, w_function, g_function) - .and_also(|| match_given_type(state, w_argument, g_argument)), + ) => match_given_type(state, context, w_function, g_function) + .and_also(|| match_given_type(state, context, w_argument, g_argument)), ( &Type::OperatorApplication(f1, t1, l1, r1), &Type::OperatorApplication(f2, t2, l2, r2), ) => { if f1 == f2 && t1 == t2 { - match_given_type(state, l1, l2).and_also(|| match_given_type(state, r1, r2)) + match_given_type(state, context, l1, l2) + .and_also(|| match_given_type(state, context, r1, r2)) } else { MatchType::Apart } @@ -750,7 +783,7 @@ fn match_given_type(state: &mut CheckState, wanted: TypeId, given: TypeId) -> Ma let a1 = Arc::clone(a1); let a2 = Arc::clone(a2); iter::zip(a1.iter(), a2.iter()).fold(MatchType::Match, |result, (&a1, &a2)| { - result.and_also(|| match_given_type(state, a1, a2)) + result.and_also(|| match_given_type(state, context, a1, a2)) }) } else { MatchType::Apart @@ -800,6 +833,11 @@ fn can_unify(state: &mut CheckState, t1: TypeId, t2: TypeId) -> CanUnify { .and_also(|| can_unify(state, t1_result, t2_result)) } + // Function(a, b) and Application(Application(head, a), b) can potentially unify + // when head resolves to the Function constructor. + (&Type::Function(..), &Type::Application(..)) + | (&Type::Application(..), &Type::Function(..)) => Unify, + ( &Type::KindApplication(t1_function, t1_argument), &Type::KindApplication(t2_function, t2_argument), @@ -964,7 +1002,7 @@ where /// Matches a wanted constraint to given constraints. fn match_given_instances( state: &mut CheckState, - _context: &CheckContext, + context: &CheckContext, wanted: &ConstraintApplication, given: &[ConstraintApplication], ) -> QueryResult> @@ -985,7 +1023,7 @@ where for (index, (&wanted_argument, &given_argument)) in wanted.arguments.iter().zip(&given.arguments).enumerate() { - let match_result = match_given_type(state, wanted_argument, given_argument); + let match_result = match_given_type(state, context, wanted_argument, given_argument); if matches!(match_result, MatchType::Apart) { continue 'given; diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 3476d4c7..78e54594 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -108,50 +108,16 @@ where && subtype_with_mode(state, context, t1_result, t2_result, ElaborationMode::No)?) } - (Type::Application(t1_partial, t1_result), Type::Function(t2_argument, t2_result)) => { - let t1_partial = state.normalize_type(t1_partial); - if let Type::Application(t1_constructor, t1_argument) = state.storage[t1_partial] { - Ok(unify(state, context, t1_constructor, context.prim.function)? - && subtype_with_mode( - state, - context, - t2_argument, - t1_argument, - ElaborationMode::No, - )? - && subtype_with_mode( - state, - context, - t1_result, - t2_result, - ElaborationMode::No, - )?) - } else { - unify(state, context, t1, t2) - } + (Type::Application(_, _), Type::Function(t2_argument, t2_result)) => { + let t2 = state.storage.intern(Type::Application(context.prim.function, t2_argument)); + let t2 = state.storage.intern(Type::Application(t2, t2_result)); + subtype_with_mode(state, context, t1, t2, mode) } - (Type::Function(t1_argument, t1_result), Type::Application(t2_partial, t2_result)) => { - let t2_partial = state.normalize_type(t2_partial); - if let Type::Application(t2_constructor, t2_argument) = state.storage[t2_partial] { - Ok(unify(state, context, t2_constructor, context.prim.function)? - && subtype_with_mode( - state, - context, - t2_argument, - t1_argument, - ElaborationMode::No, - )? - && subtype_with_mode( - state, - context, - t1_result, - t2_result, - ElaborationMode::No, - )?) - } else { - unify(state, context, t1, t2) - } + (Type::Function(t1_argument, t1_result), Type::Application(_, _)) => { + let t1 = state.storage.intern(Type::Application(context.prim.function, t1_argument)); + let t1 = state.storage.intern(Type::Application(t1, t1_result)); + subtype_with_mode(state, context, t1, t2, mode) } (_, Type::Forall(ref binder, inner)) => { @@ -259,26 +225,19 @@ where // monomorphic = identity // // Unifying `?a ?t ?t` and `a -> a` solves `?a := Function`. - (Type::Application(t1_partial, t1_result), Type::Function(t2_argument, t2_result)) => { - let t1_partial = state.normalize_type(t1_partial); - if let Type::Application(t1_constructor, t1_argument) = state.storage[t1_partial] { - unify(state, context, t1_constructor, context.prim.function)? - && unify(state, context, t1_argument, t2_argument)? - && unify(state, context, t1_result, t2_result)? - } else { - false - } + // + // We reconstruct the `Application`-based form for a `Function` as the + // type to unify against, allowing `Application(?f, ?x)` to unify. + (Type::Application(_, _), Type::Function(t2_argument, t2_result)) => { + let t2 = state.storage.intern(Type::Application(context.prim.function, t2_argument)); + let t2 = state.storage.intern(Type::Application(t2, t2_result)); + unify(state, context, t1, t2)? } - (Type::Function(t1_argument, t1_result), Type::Application(t2_partial, t2_result)) => { - let t2_partial = state.normalize_type(t2_partial); - if let Type::Application(t2_constructor, t2_argument) = state.storage[t2_partial] { - unify(state, context, t2_constructor, context.prim.function)? - && unify(state, context, t1_argument, t2_argument)? - && unify(state, context, t1_result, t2_result)? - } else { - false - } + (Type::Function(t1_argument, t1_result), Type::Application(_, _)) => { + let t1 = state.storage.intern(Type::Application(context.prim.function, t1_argument)); + let t1 = state.storage.intern(Type::Application(t1, t1_result)); + unify(state, context, t1, t2)? } (Type::Row(t1_row), Type::Row(t2_row)) => unify_rows(state, context, t1_row, t2_row)?, diff --git a/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.purs b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.purs new file mode 100644 index 00000000..53105fa8 --- /dev/null +++ b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Data.Newtype (class Newtype, unwrap) + +newtype Endo :: forall k. (k -> k -> Type) -> k -> Type +newtype Endo c a = Endo (c a a) + +instance Newtype (Endo c a) (c a a) + +test :: forall b. Endo Function b -> b -> b +test x = unwrap x diff --git a/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap new file mode 100644 index 00000000..58879edd --- /dev/null +++ b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Endo :: + forall (k :: Type) (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)). + (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)) (a :: (k :: Type)) -> + Endo @(k :: Type) (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)) +test :: forall (b :: Type). Endo @Type Function (b :: Type) -> (b :: Type) -> (b :: Type) + +Types +Endo :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> (k :: Type) -> Type + +Data +Endo + Quantified = :0 + Kind = :1 + + +Roles +Endo = [Representational, Nominal] + +Instances +instance forall (&0 :: Type). Newtype (Endo @(&0 :: Type) (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) (&2 :: (&0 :: Type)) :: Type) ((&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) (&2 :: (&0 :: Type)) (&2 :: (&0 :: Type)) :: Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 503754a1..c7cfbe7b 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -623,3 +623,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_303_instance_given_constraint_main() { run_test("303_instance_given_constraint", "Main"); } #[rustfmt::skip] #[test] fn test_305_type_operator_unification_main() { run_test("305_type_operator_unification", "Main"); } + +#[rustfmt::skip] #[test] fn test_306_kind_application_instance_matching_main() { run_test("306_kind_application_instance_matching", "Main"); } From 5b63bb549ecff234d05fa8f4ca3abd78199d3e45 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 15:32:18 +0800 Subject: [PATCH 128/386] Implement proper local-scope solving for constraints --- compiler-core/checking/src/algorithm/state.rs | 1 + compiler-core/checking/src/algorithm/term.rs | 29 ++++++++++++------- .../307_where_let_interaction/Main.purs | 18 ++++++++++++ .../307_where_let_interaction/Main.snap | 19 ++++++++++++ .../308_let_constraint_scoping/Main.purs | 17 +++++++++++ .../308_let_constraint_scoping/Main.snap | 18 ++++++++++++ tests-integration/tests/checking/generated.rs | 4 +++ 7 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 tests-integration/fixtures/checking/307_where_let_interaction/Main.purs create mode 100644 tests-integration/fixtures/checking/307_where_let_interaction/Main.snap create mode 100644 tests-integration/fixtures/checking/308_let_constraint_scoping/Main.purs create mode 100644 tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index f998154c..4aaf18e8 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1047,6 +1047,7 @@ impl CheckState { residuals } + pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { if let Some(patterns) = exhaustiveness.missing { let patterns = Arc::from(patterns); diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 747ad069..1a3c7a82 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1,4 +1,5 @@ use std::iter; +use std::mem; use building_types::QueryResult; use itertools::{Itertools, Position}; @@ -1749,11 +1750,19 @@ fn check_let_name_binding( where Q: ExternalQueries, { - state.with_local_givens(|state| { - state.with_error_step(ErrorStep::CheckingLetName(id), |state| { - check_let_name_binding_core(state, context, id) - }) - }) + let outer_wanted = mem::take(&mut state.constraints.wanted); + let outer_given = mem::take(&mut state.constraints.given); + + let result = state.with_error_step(ErrorStep::CheckingLetName(id), |state| { + check_let_name_binding_core(state, context, id) + }); + + // Residuals from solving are the only wanteds remaining. + let residual = mem::replace(&mut state.constraints.wanted, outer_wanted); + state.constraints.given = outer_given; + state.constraints.wanted.extend(residual); + + result } fn check_let_name_binding_core( @@ -1791,12 +1800,12 @@ where let origin = equation::ExhaustivenessOrigin::FromType(name_type); equation::patterns(state, context, origin, &name.equations)?; - }; + } - // PureScript does not have let generalisation; residuals are moved - // to the parent scope's wanted constraints. Given constraints must - // also be preserved across let bindings. This is demonstrated by - // 274_givens_retained, 275_givens_scoped + // PureScript does not generalise let bindings. Constraints propagate + // to the enclosing declaration where they are solved via dictionary + // passing. Both wanteds and givens are scoped to this binding by + // the caller, so the standard solver only sees local constraints. let residual = equation::constraints(state, context, equation::ConstraintsPolicy::Return)?; state.constraints.extend_wanted(&residual); diff --git a/tests-integration/fixtures/checking/307_where_let_interaction/Main.purs b/tests-integration/fixtures/checking/307_where_let_interaction/Main.purs new file mode 100644 index 00000000..5975571f --- /dev/null +++ b/tests-integration/fixtures/checking/307_where_let_interaction/Main.purs @@ -0,0 +1,18 @@ +module Main where + +class MyClass a where + method :: a -> Int + +instance MyClass Int where + method _ = 42 + +test :: forall a. MyClass a => a -> Int +test _ = + let go x = method x + in go 42 + +test2 :: forall a. MyClass a => a -> Int +test2 _ = + let go :: _ -> _ + go x = method x + in go 42 diff --git a/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap b/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap new file mode 100644 index 00000000..a7ba5715 --- /dev/null +++ b/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap @@ -0,0 +1,19 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +method :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int +test :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int +test2 :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int + +Types +MyClass :: Type -> Constraint + +Classes +class MyClass (&0 :: Type) + +Instances +instance MyClass (Int :: Type) + chain: 0 diff --git a/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.purs b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.purs new file mode 100644 index 00000000..8a627f95 --- /dev/null +++ b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.purs @@ -0,0 +1,17 @@ +module Main where + +class MyClass a where + method :: a -> Int + +instance MyClass Int where + method _ = 42 + +test :: forall a. MyClass a => a -> Int +test x = + let + bar y = method y + + baz :: MyClass Int => Int + baz = method 42 + in + bar x diff --git a/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap new file mode 100644 index 00000000..3c25ed80 --- /dev/null +++ b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +method :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int +test :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int + +Types +MyClass :: Type -> Constraint + +Classes +class MyClass (&0 :: Type) + +Instances +instance MyClass (Int :: Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index c7cfbe7b..ff1660aa 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -625,3 +625,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_305_type_operator_unification_main() { run_test("305_type_operator_unification", "Main"); } #[rustfmt::skip] #[test] fn test_306_kind_application_instance_matching_main() { run_test("306_kind_application_instance_matching", "Main"); } + +#[rustfmt::skip] #[test] fn test_307_where_let_interaction_main() { run_test("307_where_let_interaction", "Main"); } + +#[rustfmt::skip] #[test] fn test_308_let_constraint_scoping_main() { run_test("308_let_constraint_scoping", "Main"); } From 76cb113ea5c08b57f3738704b4a009389f709e50 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 17:56:02 +0800 Subject: [PATCH 129/386] Separate type classes from non-class types in the resolver Add a parallel `classes` map alongside `types` in ResolvedLocals, ResolvedExports, and ResolvedImport. Classes are routed exclusively into `classes`, non-class types into `types`. The lowering phase uses context-aware lookup: an `in_constraint` flag propagates through TypeConstrained and TypeApplicationChain so that TypeConstructor tries class resolution first in constraint position, falling back to type resolution. The checking phase's PrimLookup and known-type collectors use dedicated class lookups for class items (Partial, Add, Warn, Coercible, etc.). LSP consumers (completion, definition, hover, references, symbols) iterate and look up both types and classes. This fixes the conflict where Prim.Row.Cons (a class) and Prim.RowList.Cons (a data type) occupied the same map, reducing the record package compatibility errors from 39 to 3. Co-authored-by: Amp --- compiler-core/checking/src/algorithm.rs | 9 +- .../checking/src/algorithm/constraint.rs | 2 +- compiler-core/checking/src/algorithm/state.rs | 85 ++++++----- compiler-core/lowering/src/algorithm.rs | 49 ++++++- .../lowering/src/algorithm/recursive.rs | 24 +++- compiler-core/resolving/src/algorithm.rs | 132 +++++++++++++++++- compiler-core/resolving/src/lib.rs | 49 ++++++- compiler-lsp/analyzer/src/completion/edit.rs | 1 + .../analyzer/src/completion/prelude.rs | 1 + .../analyzer/src/completion/sources.rs | 83 ++++++++++- compiler-lsp/analyzer/src/definition.rs | 19 ++- compiler-lsp/analyzer/src/hover.rs | 19 ++- compiler-lsp/analyzer/src/references.rs | 21 ++- compiler-lsp/analyzer/src/symbols.rs | 16 +++ .../lsp/009_completion_suggestion/Main.snap | 7 +- .../lsp/032_completion_cache_exact/Main.snap | 41 +++--- .../lsp/033_completion_cache_prefix/Main.snap | 47 ++++--- .../001_local_resolution/Explicit.snap | 9 +- .../001_local_resolution/ExplicitSelf.snap | 9 +- .../001_local_resolution/Implicit.snap | 9 +- .../002_import_resolution/ImportExplicit.snap | 13 ++ .../ImportForLocalOnly.snap | 9 ++ .../ImportHiddenConstructor.snap | 7 + .../ImportQualifiedExplicit.snap | 7 + .../ImportQualifiedHiding.snap | 7 + .../ImportQualifiedImplicit.snap | 7 + .../ImportUnqualifiedExplicit.snap | 7 + .../ImportUnqualifiedHiding.snap | 7 + .../ImportUnqualifiedImplicit.snap | 7 + .../002_import_resolution/Library.snap | 5 + .../LibraryExplicit.snap | 5 + .../003_import_errors/DuplicateLocal.snap | 5 + .../DuplicateQualifiedImport.snap | 7 + .../003_import_errors/InvalidConstructor.snap | 5 + .../003_import_errors/InvalidImport.snap | 9 ++ .../resolving/003_import_errors/LibraryA.snap | 5 + .../resolving/003_import_errors/LibraryB.snap | 5 + .../Internal.snap | 5 + .../Library.snap | 7 + .../Main.snap | 7 + .../005_class_members/ClassLibrary.snap | 5 + .../005_class_members/HiddenClass.snap | 7 + .../005_class_members/ImportedClass.snap | 7 + .../005_class_members/LocalClass.snap | 5 + .../005_class_members/ReExportConsumer.snap | 7 + .../005_class_members/ReExporter.snap | 7 + tests-integration/src/generated/basic.rs | 30 ++++ 47 files changed, 723 insertions(+), 113 deletions(-) diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index e5cbfef2..03b58489 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -420,6 +420,11 @@ pub fn check_prim(queries: &impl ExternalQueries, file_id: FileId) -> QueryResul prim_type.unwrap_or_else(|| unreachable!("invariant violated: {name} not in Prim")) }; + let lookup_class = |name: &str| { + let prim_class = resolved.exports.lookup_class(name); + prim_class.unwrap_or_else(|| unreachable!("invariant violated: {name} not in Prim")) + }; + let type_core = { let (file_id, item_id) = lookup_type("Type"); queries.intern_type(Type::Constructor(file_id, item_id)) @@ -455,11 +460,13 @@ pub fn check_prim(queries: &impl ExternalQueries, file_id: FileId) -> QueryResul insert_type("String", type_core); insert_type("Char", type_core); insert_type("Boolean", type_core); - insert_type("Partial", constraint_core); insert_type("Constraint", type_core); insert_type("Symbol", type_core); insert_type("Row", type_to_type_core); + let (_, partial_id) = lookup_class("Partial"); + checked_module.types.insert(partial_id, constraint_core); + let mut insert_roles = |name: &str, roles: &[Role]| { let (_, item_id) = lookup_type(name); checked_module.roles.insert(item_id, Arc::from(roles)); diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 96a874c6..9dc60bc0 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -161,7 +161,7 @@ where let mut applications = applications.collect_vec(); let is_coercible = |file_id, item_id| { - (&context.prim_coerce).file_id == file_id && (&context.prim_coerce).coercible == item_id + context.prim_coerce.file_id == file_id && context.prim_coerce.coercible == item_id }; // For coercible applications, also elaborate into symmetric versions. diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 4aaf18e8..50514252 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -481,6 +481,22 @@ impl<'r, 's> PrimLookup<'r, 's> { self.storage.intern(Type::Constructor(file_id, type_id)) } + fn class_item(&self, name: &str) -> TypeItemId { + let (_, type_id) = + self.resolved.lookup_class(self.resolved, None, name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + type_id + } + + fn class_constructor(&mut self, name: &str) -> TypeId { + let (file_id, type_id) = + self.resolved.lookup_class(self.resolved, None, name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + self.storage.intern(Type::Constructor(file_id, type_id)) + } + fn intern(&mut self, ty: Type) -> TypeId { self.storage.intern(ty) } @@ -535,7 +551,7 @@ impl PrimCore { string: lookup.type_constructor("String"), char: lookup.type_constructor("Char"), boolean: lookup.type_constructor("Boolean"), - partial: lookup.type_constructor("Partial"), + partial: lookup.class_constructor("Partial"), constraint: lookup.type_constructor("Constraint"), symbol: lookup.type_constructor("Symbol"), row, @@ -564,10 +580,10 @@ impl PrimIntCore { Ok(PrimIntCore { file_id, - add: lookup.type_item("Add"), - mul: lookup.type_item("Mul"), - compare: lookup.type_item("Compare"), - to_string: lookup.type_item("ToString"), + add: lookup.class_item("Add"), + mul: lookup.class_item("Mul"), + compare: lookup.class_item("Compare"), + to_string: lookup.class_item("ToString"), }) } } @@ -623,9 +639,9 @@ impl PrimSymbolCore { Ok(PrimSymbolCore { file_id, - append: lookup.type_item("Append"), - compare: lookup.type_item("Compare"), - cons: lookup.type_item("Cons"), + append: lookup.class_item("Append"), + compare: lookup.class_item("Compare"), + cons: lookup.class_item("Cons"), }) } } @@ -669,10 +685,10 @@ impl PrimRowCore { Ok(PrimRowCore { file_id, - union: lookup.type_item("Union"), - cons: lookup.type_item("Cons"), - lacks: lookup.type_item("Lacks"), - nub: lookup.type_item("Nub"), + union: lookup.class_item("Union"), + cons: lookup.class_item("Cons"), + lacks: lookup.class_item("Lacks"), + nub: lookup.class_item("Nub"), }) } } @@ -698,7 +714,7 @@ impl PrimRowListCore { Ok(PrimRowListCore { file_id, - row_to_list: lookup.type_item("RowToList"), + row_to_list: lookup.class_item("RowToList"), cons: lookup.type_constructor("Cons"), nil: lookup.type_constructor("Nil"), }) @@ -719,7 +735,7 @@ impl PrimCoerceCore { let resolved = queries.resolved(file_id)?; let (_, coercible) = resolved .exports - .lookup_type("Coercible") + .lookup_class("Coercible") .unwrap_or_else(|| unreachable!("invariant violated: Coercible not in Prim.Coerce")); Ok(PrimCoerceCore { file_id, coercible }) @@ -748,8 +764,8 @@ impl PrimTypeErrorCore { Ok(PrimTypeErrorCore { file_id, - warn: lookup.type_item("Warn"), - fail: lookup.type_item("Fail"), + warn: lookup.class_item("Warn"), + fail: lookup.class_item("Fail"), text: lookup.type_constructor("Text"), quote: lookup.type_constructor("Quote"), quote_label: lookup.type_constructor("QuoteLabel"), @@ -774,7 +790,7 @@ fn fetch_known_term( Ok(Some((file_id, term_id))) } -fn fetch_known_type( +fn fetch_known_class( queries: &impl ExternalQueries, m: &str, n: &str, @@ -783,7 +799,7 @@ fn fetch_known_type( return Ok(None); }; let resolved = queries.resolved(file_id)?; - let Some((file_id, type_id)) = resolved.exports.lookup_type(n) else { + let Some((file_id, type_id)) = resolved.exports.lookup_class(n) else { return Ok(None); }; Ok(Some((file_id, type_id))) @@ -824,21 +840,21 @@ pub struct KnownTypesCore { impl KnownTypesCore { fn collect(queries: &impl ExternalQueries) -> QueryResult { - let eq = fetch_known_type(queries, "Data.Eq", "Eq")?; - let eq1 = fetch_known_type(queries, "Data.Eq", "Eq1")?; - let ord = fetch_known_type(queries, "Data.Ord", "Ord")?; - let ord1 = fetch_known_type(queries, "Data.Ord", "Ord1")?; - let functor = fetch_known_type(queries, "Data.Functor", "Functor")?; - let bifunctor = fetch_known_type(queries, "Data.Bifunctor", "Bifunctor")?; + let eq = fetch_known_class(queries, "Data.Eq", "Eq")?; + let eq1 = fetch_known_class(queries, "Data.Eq", "Eq1")?; + let ord = fetch_known_class(queries, "Data.Ord", "Ord")?; + let ord1 = fetch_known_class(queries, "Data.Ord", "Ord1")?; + let functor = fetch_known_class(queries, "Data.Functor", "Functor")?; + let bifunctor = fetch_known_class(queries, "Data.Bifunctor", "Bifunctor")?; let contravariant = - fetch_known_type(queries, "Data.Functor.Contravariant", "Contravariant")?; - let profunctor = fetch_known_type(queries, "Data.Profunctor", "Profunctor")?; - let foldable = fetch_known_type(queries, "Data.Foldable", "Foldable")?; - let bifoldable = fetch_known_type(queries, "Data.Bifoldable", "Bifoldable")?; - let traversable = fetch_known_type(queries, "Data.Traversable", "Traversable")?; - let bitraversable = fetch_known_type(queries, "Data.Bitraversable", "Bitraversable")?; - let newtype = fetch_known_type(queries, "Data.Newtype", "Newtype")?; - let generic = fetch_known_type(queries, "Data.Generic.Rep", "Generic")?; + fetch_known_class(queries, "Data.Functor.Contravariant", "Contravariant")?; + let profunctor = fetch_known_class(queries, "Data.Profunctor", "Profunctor")?; + let foldable = fetch_known_class(queries, "Data.Foldable", "Foldable")?; + let bifoldable = fetch_known_class(queries, "Data.Bifoldable", "Bifoldable")?; + let traversable = fetch_known_class(queries, "Data.Traversable", "Traversable")?; + let bitraversable = fetch_known_class(queries, "Data.Bitraversable", "Bitraversable")?; + let newtype = fetch_known_class(queries, "Data.Newtype", "Newtype")?; + let generic = fetch_known_class(queries, "Data.Generic.Rep", "Generic")?; Ok(KnownTypesCore { eq, eq1, @@ -869,8 +885,8 @@ impl KnownReflectableCore { queries: &impl ExternalQueries, storage: &mut TypeInterner, ) -> QueryResult { - let is_symbol = fetch_known_type(queries, "Data.Symbol", "IsSymbol")?; - let reflectable = fetch_known_type(queries, "Data.Reflectable", "Reflectable")?; + let is_symbol = fetch_known_class(queries, "Data.Symbol", "IsSymbol")?; + let reflectable = fetch_known_class(queries, "Data.Reflectable", "Reflectable")?; let ordering = fetch_known_constructor(queries, storage, "Data.Ordering", "Ordering")?; Ok(KnownReflectableCore { is_symbol, reflectable, ordering }) } @@ -1047,7 +1063,6 @@ impl CheckState { residuals } - pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { if let Some(patterns) = exhaustiveness.missing { let patterns = Arc::from(patterns); diff --git a/compiler-core/lowering/src/algorithm.rs b/compiler-core/lowering/src/algorithm.rs index c0235b3d..0a9018ad 100644 --- a/compiler-core/lowering/src/algorithm.rs +++ b/compiler-core/lowering/src/algorithm.rs @@ -43,6 +43,8 @@ pub(crate) struct State { pub(crate) synonym_edges: FxHashSet<(TypeItemId, TypeItemId)>, pub(crate) let_binding_graph: ItemGraph, + pub(crate) in_constraint: bool, + pub(crate) errors: Vec, } @@ -266,6 +268,33 @@ impl State { Some((file_id, type_id)) } + fn resolve_class_reference( + &mut self, + context: &Context, + qualifier: Option<&str>, + name: &str, + ) -> Option<(FileId, TypeItemId)> { + let (file_id, type_id) = context.lookup_class(qualifier, name)?; + + if context.file_id == file_id + && let Some(current_id) = self.current_type + { + self.type_edges.insert((current_id, type_id)); + + if let Some(synonym_id) = self.current_synonym + && let TypeItemKind::Synonym { .. } = context.indexed.items[type_id].kind + { + self.synonym_edges.insert((synonym_id, type_id)); + } + + if let Some(kind_id) = self.current_kind { + self.kind_edges.insert((kind_id, type_id)); + } + } + + Some((file_id, type_id)) + } + fn resolve_type_variable(&mut self, id: TypeId, name: &str) -> Option { let node = self.graph_scope?; if let GraphNode::Implicit { collecting, bindings, .. } = &mut self.graph.inner[node] { @@ -324,6 +353,16 @@ impl Context<'_> { let name = name.as_ref(); self.resolved.lookup_type(self.prim, qualifier, name) } + + fn lookup_class(&self, qualifier: Option, name: N) -> Option<(FileId, TypeItemId)> + where + Q: AsRef, + N: AsRef, + { + let qualifier = qualifier.as_ref().map(Q::as_ref); + let name = name.as_ref(); + self.resolved.lookup_class(self.prim, qualifier, name) + } } pub(super) fn lower_module( @@ -372,7 +411,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it let qualified = head.qualified()?; let (qualifier, name) = recursive::lower_qualified_name(&qualified, cst::QualifiedName::upper)?; - state.resolve_type_reference(context, qualifier.as_deref(), &name) + state.resolve_class_reference(context, qualifier.as_deref(), &name) }); state.push_implicit_scope(); @@ -385,6 +424,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it .collect() }; + state.in_constraint = true; let constraints = recover! { cst.as_ref()? .instance_constraints()? @@ -392,6 +432,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it .map(|cst| recursive::lower_type(state, context, &cst)) .collect() }; + state.in_constraint = false; state.finish_implicit_scope(); let kind = TermItemIr::Derive { newtype, constraints, resolution, arguments }; @@ -418,7 +459,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it let qualified = head.qualified()?; let (qualifier, name) = recursive::lower_qualified_name(&qualified, cst::QualifiedName::upper)?; - state.resolve_type_reference(context, qualifier.as_deref(), &name) + state.resolve_class_reference(context, qualifier.as_deref(), &name) }); state.push_implicit_scope(); @@ -431,6 +472,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it .collect() }; + state.in_constraint = true; let constraints = recover! { cst.as_ref()? .instance_constraints()? @@ -438,6 +480,7 @@ fn lower_term_item(state: &mut State, context: &Context, item_id: TermItemId, it .map(|cst| recursive::lower_type(state, context, &cst)) .collect() }; + state.in_constraint = false; state.finish_implicit_scope(); let members = recover! { @@ -634,12 +677,14 @@ fn lower_type_item(state: &mut State, context: &Context, item_id: TypeItemId, it .collect() }; + state.in_constraint = true; let constraints = recover! { cst.class_constraints()? .children() .map(|cst| recursive::lower_type(state, context, &cst)) .collect() }; + state.in_constraint = false; let variable_map: FxHashMap<&str, u8> = variables .iter() diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index 6cbc2289..357acdd7 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -890,9 +890,11 @@ fn lower_type_kind( ) -> TypeKind { match cst { cst::Type::TypeApplicationChain(cst) => { - let mut children = cst.children().map(|cst| lower_type(state, context, &cst)); - let function = children.next(); - let arguments = children.collect(); + let mut children = cst.children(); + let function = children.next().map(|cst| lower_type(state, context, &cst)); + let in_constraint = mem::replace(&mut state.in_constraint, false); + let arguments = children.map(|cst| lower_type(state, context, &cst)).collect(); + state.in_constraint = in_constraint; TypeKind::ApplicationChain { function, arguments } } cst::Type::TypeArrow(cst) => { @@ -902,15 +904,23 @@ fn lower_type_kind( TypeKind::Arrow { argument, result } } cst::Type::TypeConstrained(cst) => { - let mut children = cst.children().map(|cst| lower_type(state, context, &cst)); - let constraint = children.next(); - let constrained = children.next(); + let mut children = cst.children(); + let in_constraint = mem::replace(&mut state.in_constraint, true); + let constraint = children.next().map(|cst| lower_type(state, context, &cst)); + state.in_constraint = in_constraint; + let constrained = children.next().map(|cst| lower_type(state, context, &cst)); TypeKind::Constrained { constraint, constrained } } cst::Type::TypeConstructor(cst) => { let resolution = cst.name().and_then(|cst| { let (qualifier, name) = lower_qualified_name(&cst, cst::QualifiedName::upper)?; - state.resolve_type_reference(context, qualifier.as_deref(), &name) + if state.in_constraint { + state.resolve_class_reference(context, qualifier.as_deref(), &name).or_else( + || state.resolve_type_reference(context, qualifier.as_deref(), &name), + ) + } else { + state.resolve_type_reference(context, qualifier.as_deref(), &name) + } }); if resolution.is_none() { let id = context.stabilized.lookup_cst(cst).expect_id(); diff --git a/compiler-core/resolving/src/algorithm.rs b/compiler-core/resolving/src/algorithm.rs index 214d0945..10afb5a4 100644 --- a/compiler-core/resolving/src/algorithm.rs +++ b/compiler-core/resolving/src/algorithm.rs @@ -103,9 +103,12 @@ fn resolve_import( let terms = import_resolved.exports.iter_terms().map(|(name, file, id)| (name, file, id, kind)); let types = import_resolved.exports.iter_types().map(|(name, file, id)| (name, file, id, kind)); + let classes = + import_resolved.exports.iter_classes().map(|(name, file, id)| (name, file, id, kind)); add_imported_terms(errors, resolved, indexing_import_id, terms); add_imported_types(errors, resolved, indexing_import_id, types); + add_imported_classes(errors, resolved, indexing_import_id, classes); // Adjust import kinds for explicit/hidden imports BEFORE copying class members if !matches!(indexing_import.kind, ImportKind::Implicit) { @@ -123,6 +126,8 @@ fn resolve_import( let Some(implicit) = implicit else { continue }; let item = (*file, *id, implicit); resolve_implicit(queries, resolved, indexing_import, item)?; + } else if let Some((_, _, kind)) = resolved.classes.get_mut(name) { + *kind = indexing_import.kind; } else { errors.push(ResolvingError::InvalidImportItem { id }); }; @@ -130,7 +135,7 @@ fn resolve_import( } // Copy class members AFTER kind adjustments so hidden types are properly filtered - for (_, _, type_id, import_kind) in resolved.iter_types() { + for (_, _, type_id, import_kind) in resolved.iter_classes() { if matches!(import_kind, ImportKind::Hidden) { continue; } @@ -203,6 +208,19 @@ fn add_imported_types<'a>( }); } +fn add_imported_classes<'a>( + errors: &mut Vec, + resolved: &mut ResolvedImport, + indexing_import_id: ImportId, + terms: impl Iterator, +) { + let (additional, _) = terms.size_hint(); + resolved.classes.reserve(additional); + terms.for_each(|(name, file, id, kind)| { + add_imported_class(errors, resolved, indexing_import_id, name, file, id, kind); + }); +} + fn add_imported_term( errors: &mut Vec, resolved: &mut ResolvedImport, @@ -245,6 +263,27 @@ fn add_imported_type( } } +fn add_imported_class( + errors: &mut Vec, + resolved: &mut ResolvedImport, + indexing_import_id: ImportId, + name: &SmolStr, + file: FileId, + id: TypeItemId, + kind: ImportKind, +) { + if let Some((existing_file, existing_term, _)) = resolved.classes.get(name) { + let duplicate = (file, id, indexing_import_id); + let existing = (*existing_file, *existing_term, resolved.id); + if duplicate != existing { + errors.push(ResolvingError::TypeImportConflict { existing, duplicate }); + } + } else { + let name = SmolStr::clone(name); + resolved.classes.insert(name, (file, id, kind)); + } +} + fn resolve_exports(state: &mut State, indexed: &IndexedModule, file: FileId) { export_module_items(state, indexed, file); export_module_imports(state, indexed); @@ -326,6 +365,36 @@ fn add_local_type( } } +fn add_local_classes<'k>( + items: &mut ResolvedLocals, + errors: &mut Vec, + iterator: impl Iterator, +) { + let (additional, _) = iterator.size_hint(); + items.classes.reserve(additional); + iterator.for_each(move |(name, file, id)| { + add_local_class(items, errors, name, file, id); + }); +} + +fn add_local_class( + items: &mut ResolvedLocals, + errors: &mut Vec, + name: &SmolStr, + file: FileId, + id: TypeItemId, +) { + if let Some(&existing) = items.classes.get(name) { + let duplicate = (file, id); + if existing != duplicate { + errors.push(ResolvingError::ExistingType { existing, duplicate }); + } + } else { + let name = SmolStr::clone(name); + items.classes.insert(name, (file, id)); + } +} + fn export_module_items(state: &mut State, indexed: &IndexedModule, file: FileId) { let local_terms = indexed.items.iter_terms().filter_map(|(id, item)| { let name = item.name.as_ref()?; @@ -334,11 +403,18 @@ fn export_module_items(state: &mut State, indexed: &IndexedModule, file: FileId) let local_types = indexed.items.iter_types().filter_map(|(id, item)| { let name = item.name.as_ref()?; - Some((name, file, id)) + Some((name, file, id, &item.kind)) }); + let (local_class_items, local_type_items): (Vec<_>, Vec<_>) = + local_types.partition(|(_, _, _, kind)| matches!(kind, TypeItemKind::Class { .. })); + + let local_types = local_type_items.into_iter().map(|(name, file, id, _)| (name, file, id)); + let local_classes = local_class_items.into_iter().map(|(name, file, id, _)| (name, file, id)); + add_local_terms(&mut state.locals, &mut state.errors, local_terms); add_local_types(&mut state.locals, &mut state.errors, local_types); + add_local_classes(&mut state.locals, &mut state.errors, local_classes); let exported_terms = indexed.items.iter_terms().filter_map(|(id, item)| { // Instances cannot be to referred directly by their given name yet. @@ -358,11 +434,22 @@ fn export_module_items(state: &mut State, indexed: &IndexedModule, file: FileId) return None; } let name = item.name.as_ref()?; - Some((name, file, id, ExportSource::Local)) + Some((name, file, id, ExportSource::Local, &item.kind)) }); + let (exported_class_items, exported_type_items): (Vec<_>, Vec<_>) = + exported_types.partition(|(_, _, _, _, kind)| matches!(kind, TypeItemKind::Class { .. })); + + let exported_types = + exported_type_items.into_iter().map(|(name, file, id, source, _)| (name, file, id, source)); + + let exported_classes = exported_class_items + .into_iter() + .map(|(name, file, id, source, _)| (name, file, id, source)); + add_export_terms(&mut state.exports, &mut state.errors, exported_terms); add_export_types(&mut state.exports, &mut state.errors, exported_types); + add_export_classes(&mut state.exports, &mut state.errors, exported_classes); } fn export_module_imports(state: &mut State, indexed: &IndexedModule) { @@ -393,8 +480,16 @@ fn export_module_imports(state: &mut State, indexed: &IndexedModule) { None } }); + let classes = import.iter_classes().filter_map(|(k, f, i, d)| { + if matches!(d, ImportKind::Implicit | ImportKind::Explicit) { + Some((k, f, i, source)) + } else { + None + } + }); add_export_terms(&mut state.exports, &mut state.errors, terms); add_export_types(&mut state.exports, &mut state.errors, types); + add_export_classes(&mut state.exports, &mut state.errors, classes); } } @@ -459,3 +554,34 @@ fn add_export_type( items.types.insert(name, (file, id, source)); } } + +fn add_export_classes<'k>( + items: &mut ResolvedExports, + errors: &mut Vec, + iterator: impl Iterator, +) { + let (additional, _) = iterator.size_hint(); + items.classes.reserve(additional); + iterator.for_each(move |(name, file, id, source)| { + add_export_class(items, errors, name, file, id, source); + }); +} + +fn add_export_class( + items: &mut ResolvedExports, + errors: &mut Vec, + name: &SmolStr, + file: FileId, + id: TypeItemId, + source: ExportSource, +) { + if let Some(&existing) = items.classes.get(name) { + let duplicate = (file, id, source); + if existing != duplicate { + errors.push(ResolvingError::TypeExportConflict { existing, duplicate }); + } + } else { + let name = SmolStr::clone(name); + items.classes.insert(name, (file, id, source)); + } +} diff --git a/compiler-core/resolving/src/lib.rs b/compiler-core/resolving/src/lib.rs index 0fc900c1..868e4a81 100644 --- a/compiler-core/resolving/src/lib.rs +++ b/compiler-core/resolving/src/lib.rs @@ -134,6 +134,25 @@ impl ResolvedModule { } } + pub fn lookup_class( + &self, + prim: &ResolvedModule, + qualifier: Option<&str>, + name: &str, + ) -> Option<(FileId, TypeItemId)> { + if let Some(qualifier) = qualifier { + let import = self.qualified.get(qualifier)?; + let (file, id, kind) = import.lookup_class(name)?; + if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + } else { + let lookup_item = |import: &ResolvedImport| import.lookup_class(name); + let lookup_prim = || prim.exports.lookup_class(name); + None.or_else(|| self.locals.lookup_class(name)) + .or_else(|| self.lookup_unqualified(lookup_item)) + .or_else(|| self.lookup_prim_import(lookup_item, lookup_prim)) + } + } + pub fn lookup_class_member( &self, class_id: TypeItemId, @@ -181,6 +200,7 @@ type ResolvedImportsQualified = FxHashMap; pub struct ResolvedLocals { terms: FxHashMap, types: FxHashMap, + classes: FxHashMap, } impl ResolvedLocals { @@ -203,6 +223,14 @@ impl ResolvedLocals { pub fn iter_types(&self) -> impl Iterator { self.types.iter().map(|(k, (f, i))| (k, *f, *i)) } + + pub fn lookup_class(&self, name: &str) -> Option<(FileId, TypeItemId)> { + self.classes.get(name).copied() + } + + pub fn iter_classes(&self) -> impl Iterator { + self.classes.iter().map(|(k, (f, i))| (k, *f, *i)) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -215,6 +243,7 @@ pub enum ExportSource { pub struct ResolvedExports { terms: FxHashMap, types: FxHashMap, + classes: FxHashMap, } impl ResolvedExports { @@ -237,6 +266,14 @@ impl ResolvedExports { pub fn iter_types(&self) -> impl Iterator { self.types.iter().map(|(k, (f, i, _))| (k, *f, *i)) } + + pub fn lookup_class(&self, name: &str) -> Option<(FileId, TypeItemId)> { + self.classes.get(name).copied().map(|(f, i, _)| (f, i)) + } + + pub fn iter_classes(&self) -> impl Iterator { + self.classes.iter().map(|(k, (f, i, _))| (k, *f, *i)) + } } #[derive(Debug, PartialEq, Eq)] @@ -247,13 +284,15 @@ pub struct ResolvedImport { pub exported: bool, terms: FxHashMap, types: FxHashMap, + classes: FxHashMap, } impl ResolvedImport { fn new(id: ImportId, file: FileId, kind: ImportKind, exported: bool) -> ResolvedImport { let terms = FxHashMap::default(); let types = FxHashMap::default(); - ResolvedImport { id, file, kind, exported, terms, types } + let classes = FxHashMap::default(); + ResolvedImport { id, file, kind, exported, terms, types, classes } } pub fn lookup_term(&self, name: &str) -> Option<(FileId, TermItemId, ImportKind)> { @@ -277,6 +316,14 @@ impl ResolvedImport { pub fn iter_types(&self) -> impl Iterator { self.types.iter().map(|(k, (f, i, d))| (k, *f, *i, *d)) } + + pub fn lookup_class(&self, name: &str) -> Option<(FileId, TypeItemId, ImportKind)> { + self.classes.get(name).copied() + } + + pub fn iter_classes(&self) -> impl Iterator { + self.classes.iter().map(|(k, (f, i, d))| (k, *f, *i, *d)) + } } pub fn resolve_module(queries: &impl ExternalQueries, file: FileId) -> QueryResult { diff --git a/compiler-lsp/analyzer/src/completion/edit.rs b/compiler-lsp/analyzer/src/completion/edit.rs index 628fe7e5..b211ddcd 100644 --- a/compiler-lsp/analyzer/src/completion/edit.rs +++ b/compiler-lsp/analyzer/src/completion/edit.rs @@ -125,6 +125,7 @@ pub(super) fn type_import_item( |import| { import .lookup_type(type_name) + .or_else(|| import.lookup_class(type_name)) .and_then(|(f, t, k)| if (f, t) == (file_id, type_id) { Some(k) } else { None }) }, |import_indexed| type_import_name(import_indexed, type_name, type_id), diff --git a/compiler-lsp/analyzer/src/completion/prelude.rs b/compiler-lsp/analyzer/src/completion/prelude.rs index 334a2d57..4637a12e 100644 --- a/compiler-lsp/analyzer/src/completion/prelude.rs +++ b/compiler-lsp/analyzer/src/completion/prelude.rs @@ -75,6 +75,7 @@ impl Context<'_, '_> { pub fn has_type_import(&self, qualifier: Option<&str>, name: &str) -> bool { self.resolved.lookup_type(self.prim_resolved, qualifier, name).is_some() + || self.resolved.lookup_class(self.prim_resolved, qualifier, name).is_some() } } diff --git a/compiler-lsp/analyzer/src/completion/sources.rs b/compiler-lsp/analyzer/src/completion/sources.rs index 9e0bc696..9e3ddefe 100644 --- a/compiler-lsp/analyzer/src/completion/sources.rs +++ b/compiler-lsp/analyzer/src/completion/sources.rs @@ -117,6 +117,22 @@ impl CompletionSource for LocalTypes { items.push(item.build()) } + let source = context.resolved.locals.iter_classes(); + let source = source.filter(move |(name, _, _)| filter.matches(name)); + + for (name, file_id, type_id) in source { + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(file_id, type_id), + ); + + item.label_description("Local".to_string()); + + items.push(item.build()) + } + Ok(()) } } @@ -198,6 +214,27 @@ impl CompletionSource for ImportedTypes { items.push(item.build()) } + + let source = import.iter_classes().filter(move |(name, _, _, kind)| { + filter.matches(name) && !matches!(kind, ImportKind::Hidden) + }); + for (name, f, t, _) in source { + let (parsed, _) = context.engine.parsed(f)?; + let description = parsed.module_name().map(|name| name.to_string()); + + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(f, t), + ); + + if let Some(description) = description { + item.label_description(description); + } + + items.push(item.build()) + } } Ok(()) @@ -286,6 +323,29 @@ impl CompletionSource for QualifiedTypes<'_> { items.push(item.build()) } + let source = import.iter_classes().filter(move |(name, _, _, kind)| { + filter.matches(name) && !matches!(kind, ImportKind::Hidden) + }); + + for (name, file_id, type_id, _) in source { + let (parsed, _) = context.engine.parsed(file_id)?; + let description = parsed.module_name().map(|name| name.to_string()); + + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(file_id, type_id), + ); + + item.edit_text(format!("{}.{name}", self.0)); + if let Some(description) = description { + item.label_description(description); + } + + items.push(item.build()) + } + Ok(()) } } @@ -369,7 +429,7 @@ impl SuggestionsHelper for SuggestedTypes { fn exports( resolved: &ResolvedModule, ) -> impl Iterator { - resolved.exports.iter_types() + resolved.exports.iter_types().chain(resolved.exports.iter_classes()) } fn candidate( @@ -543,6 +603,25 @@ impl CompletionSource for PrimTypes { items.push(item.build()) } + let source = context + .prim_resolved + .exports + .iter_classes() + .filter(move |(name, _, _)| filter.matches(name)); + + for (name, file_id, type_item) in source { + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(file_id, type_item), + ); + + item.label_description("Prim".to_string()); + + items.push(item.build()) + } + Ok(()) } } @@ -603,7 +682,7 @@ impl SuggestionsHelper for QualifiedTypesSuggestions<'_> { fn exports( resolved: &ResolvedModule, ) -> impl Iterator { - resolved.exports.iter_types() + resolved.exports.iter_types().chain(resolved.exports.iter_classes()) } fn candidate( diff --git a/compiler-lsp/analyzer/src/definition.rs b/compiler-lsp/analyzer/src/definition.rs index 18fece00..ae89ae15 100644 --- a/compiler-lsp/analyzer/src/definition.rs +++ b/compiler-lsp/analyzer/src/definition.rs @@ -129,8 +129,21 @@ fn definition_import( let goto_type = |engine: &QueryEngine, files: &Files, name: &str| { let name = name.trim_start_matches("(").trim_end_matches(")"); - let (f_id, t_id) = - import_resolved.exports.lookup_type(name).ok_or(AnalyzerError::NonFatal)?; + let (f_id, t_id) = import_resolved + .exports + .lookup_type(name) + .or_else(|| import_resolved.exports.lookup_class(name)) + .ok_or(AnalyzerError::NonFatal)?; + definition_file_type(engine, files, f_id, t_id) + }; + + let goto_class = |engine: &QueryEngine, files: &Files, name: &str| { + let name = name.trim_start_matches("(").trim_end_matches(")"); + let (f_id, t_id) = import_resolved + .exports + .lookup_class(name) + .or_else(|| import_resolved.exports.lookup_type(name)) + .ok_or(AnalyzerError::NonFatal)?; definition_file_type(engine, files, f_id, t_id) }; @@ -143,7 +156,7 @@ fn definition_import( cst::ImportItem::ImportClass(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; let name = token.text(); - goto_type(engine, files, name) + goto_class(engine, files, name) } cst::ImportItem::ImportType(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; diff --git a/compiler-lsp/analyzer/src/hover.rs b/compiler-lsp/analyzer/src/hover.rs index 1cd12429..8450b18b 100644 --- a/compiler-lsp/analyzer/src/hover.rs +++ b/compiler-lsp/analyzer/src/hover.rs @@ -113,8 +113,21 @@ fn hover_import( let hover_type_import = |engine: &QueryEngine, name: &str| { let name = name.trim_start_matches("(").trim_end_matches(")"); - let (f_id, t_id) = - import_resolved.exports.lookup_type(name).ok_or(AnalyzerError::NonFatal)?; + let (f_id, t_id) = import_resolved + .exports + .lookup_type(name) + .or_else(|| import_resolved.exports.lookup_class(name)) + .ok_or(AnalyzerError::NonFatal)?; + hover_file_type(engine, f_id, t_id) + }; + + let hover_class_import = |engine: &QueryEngine, name: &str| { + let name = name.trim_start_matches("(").trim_end_matches(")"); + let (f_id, t_id) = import_resolved + .exports + .lookup_class(name) + .or_else(|| import_resolved.exports.lookup_type(name)) + .ok_or(AnalyzerError::NonFatal)?; hover_file_type(engine, f_id, t_id) }; @@ -127,7 +140,7 @@ fn hover_import( cst::ImportItem::ImportClass(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; let name = token.text(); - hover_type_import(engine, name) + hover_class_import(engine, name) } cst::ImportItem::ImportType(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; diff --git a/compiler-lsp/analyzer/src/references.rs b/compiler-lsp/analyzer/src/references.rs index 27ea5cca..18db6f66 100644 --- a/compiler-lsp/analyzer/src/references.rs +++ b/compiler-lsp/analyzer/src/references.rs @@ -132,8 +132,21 @@ fn references_import( let references_type = |engine: &QueryEngine, files: &Files, name: &str| { let name = name.trim_start_matches("(").trim_end_matches(")"); - let (f_id, t_id) = - import_resolved.exports.lookup_type(name).ok_or(AnalyzerError::NonFatal)?; + let (f_id, t_id) = import_resolved + .exports + .lookup_type(name) + .or_else(|| import_resolved.exports.lookup_class(name)) + .ok_or(AnalyzerError::NonFatal)?; + references_file_type(engine, files, current_file, f_id, t_id) + }; + + let references_class = |engine: &QueryEngine, files: &Files, name: &str| { + let name = name.trim_start_matches("(").trim_end_matches(")"); + let (f_id, t_id) = import_resolved + .exports + .lookup_class(name) + .or_else(|| import_resolved.exports.lookup_type(name)) + .ok_or(AnalyzerError::NonFatal)?; references_file_type(engine, files, current_file, f_id, t_id) }; @@ -146,7 +159,7 @@ fn references_import( cst::ImportItem::ImportClass(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; let name = token.text(); - references_type(engine, files, name) + references_class(engine, files, name) } cst::ImportItem::ImportType(cst) => { let token = cst.name_token().ok_or(AnalyzerError::NonFatal)?; @@ -387,6 +400,8 @@ fn probe_type_references( probe_workspace_imports(engine, files, current_file, file_id, |import| { import.iter_types().any(|(_, f_id, t_id, kind)| { kind != ImportKind::Hidden && (f_id, t_id) == (file_id, type_id) + }) || import.iter_classes().any(|(_, f_id, t_id, kind)| { + kind != ImportKind::Hidden && (f_id, t_id) == (file_id, type_id) }) }) } diff --git a/compiler-lsp/analyzer/src/symbols.rs b/compiler-lsp/analyzer/src/symbols.rs index d90edebf..94bb44f5 100644 --- a/compiler-lsp/analyzer/src/symbols.rs +++ b/compiler-lsp/analyzer/src/symbols.rs @@ -93,6 +93,22 @@ fn build_symbol_list( container_name: None, }); } + + for (name, _, type_id) in resolved.locals.iter_classes() { + if !name.to_lowercase().starts_with(query) { + continue; + } + let location = common::file_type_location(engine, uri.clone(), file_id, type_id)?; + symbols.push(SymbolInformation { + name: name.to_string(), + kind: SymbolKind::CLASS, + tags: None, + #[allow(deprecated)] + deprecated: None, + location, + container_name: None, + }); + } } Ok(symbols) diff --git a/tests-integration/fixtures/lsp/009_completion_suggestion/Main.snap b/tests-integration/fixtures/lsp/009_completion_suggestion/Main.snap index 4e6411a1..76daf648 100644 --- a/tests-integration/fixtures/lsp/009_completion_suggestion/Main.snap +++ b/tests-integration/fixtures/lsp/009_completion_suggestion/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/lsp/generated.rs +assertion_line: 12 expression: report --- Completion at Position { line: 3, character: 7 } @@ -93,12 +94,12 @@ cls :: C ╭──────────────┬──────────────────────────┬─────────────────────┬─────────────────────────┬───────────────┬──────────────┬────────────────────────────────────────╮ │ label │ label_detail │ label_description │ sort_text │ filter_text │ text_edit │ additional_text_edits │ ├──────────────┼──────────────────────────┼─────────────────────┼─────────────────────────┼───────────────┼──────────────┼────────────────────────────────────────┤ -│ Char │ ... │ Prim │ Char │ Char │ 16:7..16:8 │ ... │ -│ │ │ │ │ │ Char │ │ -├──────────────┼──────────────────────────┼─────────────────────┼─────────────────────────┼───────────────┼──────────────┼────────────────────────────────────────┤ │ Constraint │ ... │ Prim │ Constraint │ Constraint │ 16:7..16:8 │ ... │ │ │ │ │ │ │ Constraint │ │ ├──────────────┼──────────────────────────┼─────────────────────┼─────────────────────────┼───────────────┼──────────────┼────────────────────────────────────────┤ +│ Char │ ... │ Prim │ Char │ Char │ 16:7..16:8 │ ... │ +│ │ │ │ │ │ Char │ │ +├──────────────┼──────────────────────────┼─────────────────────┼─────────────────────────┼───────────────┼──────────────┼────────────────────────────────────────┤ │ Coercible │ (import Prim.Coerce) │ Prim.Coerce │ Prim.Coerce.Coercible │ Coercible │ 16:7..16:8 │ 1:0..1:0 │ │ │ │ │ │ │ Coercible │ import Prim.Coerce (class Coercible) │ ├──────────────┼──────────────────────────┼─────────────────────┼─────────────────────────┼───────────────┼──────────────┼────────────────────────────────────────┤ diff --git a/tests-integration/fixtures/lsp/032_completion_cache_exact/Main.snap b/tests-integration/fixtures/lsp/032_completion_cache_exact/Main.snap index 74ff9807..54143a5d 100644 --- a/tests-integration/fixtures/lsp/032_completion_cache_exact/Main.snap +++ b/tests-integration/fixtures/lsp/032_completion_cache_exact/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/lsp/generated.rs +assertion_line: 12 expression: report --- Completion at Position { line: 2, character: 14 } @@ -138,27 +139,27 @@ type Qual1 = Data.Maybe. │ Text │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Text │ Data.Maybe.Text │ 20:13..20:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Text │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 20:13..20:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 20:13..20:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Beside │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Beside │ Data.Maybe.Beside │ 20:13..20:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Beside │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 20:13..20:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 20:13..20:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ QuoteLabel │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.QuoteLabel │ Data.Maybe.QuoteLabel │ 20:13..20:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.QuoteLabel │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 20:13..20:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 20:13..20:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Doc │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Doc │ Data.Maybe.Doc │ 20:13..20:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Doc │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 20:13..20:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 20:13..20:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Maybe │ (import Data.Maybe as Data.Maybe) │ Data.Maybe │ Data.Maybe.Maybe │ Data.Maybe.Maybe │ 20:13..20:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Maybe │ import Data.Maybe as Data.Maybe │ ╰──────────────┴──────────────────────────────────────────┴─────────────────────┴─────────────────────────────┴─────────────────────────┴─────────────────────────┴───────────────────────────────────────╯ @@ -186,27 +187,27 @@ type Qual2 = Data.Maybe. │ Text │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Text │ Data.Maybe.Text │ 23:13..23:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Text │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 23:13..23:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 23:13..23:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Beside │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Beside │ Data.Maybe.Beside │ 23:13..23:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Beside │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 23:13..23:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 23:13..23:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ QuoteLabel │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.QuoteLabel │ Data.Maybe.QuoteLabel │ 23:13..23:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.QuoteLabel │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 23:13..23:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 23:13..23:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Doc │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Doc │ Data.Maybe.Doc │ 23:13..23:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Doc │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 23:13..23:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 23:13..23:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Maybe │ (import Data.Maybe as Data.Maybe) │ Data.Maybe │ Data.Maybe.Maybe │ Data.Maybe.Maybe │ 23:13..23:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Maybe │ import Data.Maybe as Data.Maybe │ ╰──────────────┴──────────────────────────────────────────┴─────────────────────┴─────────────────────────────┴─────────────────────────┴─────────────────────────┴───────────────────────────────────────╯ diff --git a/tests-integration/fixtures/lsp/033_completion_cache_prefix/Main.snap b/tests-integration/fixtures/lsp/033_completion_cache_prefix/Main.snap index 9a00f0f3..8de4baf4 100644 --- a/tests-integration/fixtures/lsp/033_completion_cache_prefix/Main.snap +++ b/tests-integration/fixtures/lsp/033_completion_cache_prefix/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/lsp/generated.rs +assertion_line: 12 expression: report --- Completion at Position { line: 2, character: 14 } @@ -36,12 +37,12 @@ type Step2 = Ma │ Qual1 │ ... │ Local │ Qual1 │ Qual1 │ 5:13..5:15 │ ... │ │ │ │ │ │ │ Qual1 │ │ ├───────────┼────────────────────────┼─────────────────────┼────────────────────┼───────────────┼──────────────┼─────────────────────────────┤ -│ Partial │ ... │ Prim │ Partial │ Partial │ 5:13..5:15 │ ... │ -│ │ │ │ │ │ Partial │ │ -├───────────┼────────────────────────┼─────────────────────┼────────────────────┼───────────────┼──────────────┼─────────────────────────────┤ │ Char │ ... │ Prim │ Char │ Char │ 5:13..5:15 │ ... │ │ │ │ │ │ │ Char │ │ ├───────────┼────────────────────────┼─────────────────────┼────────────────────┼───────────────┼──────────────┼─────────────────────────────┤ +│ Partial │ ... │ Prim │ Partial │ Partial │ 5:13..5:15 │ ... │ +│ │ │ │ │ │ Partial │ │ +├───────────┼────────────────────────┼─────────────────────┼────────────────────┼───────────────┼──────────────┼─────────────────────────────┤ │ Maybe │ (import Data.Maybe) │ Data.Maybe │ Data.Maybe.Maybe │ Maybe │ 5:13..5:15 │ 1:0..1:0 │ │ │ │ │ │ │ Maybe │ import Data.Maybe (Maybe) │ ╰───────────┴────────────────────────┴─────────────────────┴────────────────────┴───────────────┴──────────────┴─────────────────────────────╯ @@ -189,27 +190,27 @@ type Qual1 = Data.Maybe. │ Text │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Text │ Data.Maybe.Text │ 26:13..26:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Text │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 26:13..26:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 26:13..26:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Beside │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Beside │ Data.Maybe.Beside │ 26:13..26:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Beside │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 26:13..26:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 26:13..26:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ QuoteLabel │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.QuoteLabel │ Data.Maybe.QuoteLabel │ 26:13..26:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.QuoteLabel │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 26:13..26:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 26:13..26:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Doc │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Doc │ Data.Maybe.Doc │ 26:13..26:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Doc │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 26:13..26:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 26:13..26:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Maybe │ (import Data.Maybe as Data.Maybe) │ Data.Maybe │ Data.Maybe.Maybe │ Data.Maybe.Maybe │ 26:13..26:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Maybe │ import Data.Maybe as Data.Maybe │ ╰──────────────┴──────────────────────────────────────────┴─────────────────────┴─────────────────────────────┴─────────────────────────┴─────────────────────────┴───────────────────────────────────────╯ @@ -237,27 +238,27 @@ type Qual2 = Data.Maybe. │ Text │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Text │ Data.Maybe.Text │ 29:13..29:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Text │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 29:13..29:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 29:13..29:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Beside │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Beside │ Data.Maybe.Beside │ 29:13..29:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Beside │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 29:13..29:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 29:13..29:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ QuoteLabel │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.QuoteLabel │ Data.Maybe.QuoteLabel │ 29:13..29:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.QuoteLabel │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Above │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Above │ Data.Maybe.Above │ 29:13..29:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Above │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ -│ Quote │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Quote │ Data.Maybe.Quote │ 29:13..29:24 │ 1:0..1:0 │ -│ │ │ │ │ │ Data.Maybe.Quote │ import Prim.TypeError as Data.Maybe │ -├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Doc │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Doc │ Data.Maybe.Doc │ 29:13..29:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Doc │ import Prim.TypeError as Data.Maybe │ ├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Fail │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Fail │ Data.Maybe.Fail │ 29:13..29:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Fail │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ +│ Warn │ (import Prim.TypeError as Data.Maybe) │ Prim.TypeError │ Prim.TypeError.Warn │ Data.Maybe.Warn │ 29:13..29:24 │ 1:0..1:0 │ +│ │ │ │ │ │ Data.Maybe.Warn │ import Prim.TypeError as Data.Maybe │ +├──────────────┼──────────────────────────────────────────┼─────────────────────┼─────────────────────────────┼─────────────────────────┼─────────────────────────┼───────────────────────────────────────┤ │ Maybe │ (import Data.Maybe as Data.Maybe) │ Data.Maybe │ Data.Maybe.Maybe │ Data.Maybe.Maybe │ 29:13..29:24 │ 1:0..1:0 │ │ │ │ │ │ │ Data.Maybe.Maybe │ import Data.Maybe as Data.Maybe │ ╰──────────────┴──────────────────────────────────────────┴─────────────────────┴─────────────────────────────┴─────────────────────────┴─────────────────────────┴───────────────────────────────────────╯ diff --git a/tests-integration/fixtures/resolving/001_local_resolution/Explicit.snap b/tests-integration/fixtures/resolving/001_local_resolution/Explicit.snap index f3433e62..c0991dfa 100644 --- a/tests-integration/fixtures/resolving/001_local_resolution/Explicit.snap +++ b/tests-integration/fixtures/resolving/001_local_resolution/Explicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Explicit @@ -18,10 +19,12 @@ Exported Terms: Exported Types: - HiddenConstructor - Id - - Eq - Synonym - Maybe +Exported Classes: + - Eq + Local Terms: - value - eqMaybe @@ -37,10 +40,12 @@ Local Types: - HiddenConstructor - HiddenType - Id - - Eq - Synonym - Maybe +Local Classes: + - Eq + Class Members: - Eq.eq diff --git a/tests-integration/fixtures/resolving/001_local_resolution/ExplicitSelf.snap b/tests-integration/fixtures/resolving/001_local_resolution/ExplicitSelf.snap index 256bb8ac..8838a8ba 100644 --- a/tests-integration/fixtures/resolving/001_local_resolution/ExplicitSelf.snap +++ b/tests-integration/fixtures/resolving/001_local_resolution/ExplicitSelf.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ExplicitSelf @@ -21,10 +22,12 @@ Exported Types: - HiddenConstructor - HiddenType - Id - - Eq - Synonym - Maybe +Exported Classes: + - Eq + Local Terms: - value - eqMaybe @@ -40,10 +43,12 @@ Local Types: - HiddenConstructor - HiddenType - Id - - Eq - Synonym - Maybe +Local Classes: + - Eq + Class Members: - Eq.eq diff --git a/tests-integration/fixtures/resolving/001_local_resolution/Implicit.snap b/tests-integration/fixtures/resolving/001_local_resolution/Implicit.snap index a9b713c5..34276402 100644 --- a/tests-integration/fixtures/resolving/001_local_resolution/Implicit.snap +++ b/tests-integration/fixtures/resolving/001_local_resolution/Implicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Implicit @@ -21,10 +22,12 @@ Exported Types: - HiddenConstructor - HiddenType - Id - - Eq - Synonym - Maybe +Exported Classes: + - Eq + Local Terms: - value - eqMaybe @@ -40,10 +43,12 @@ Local Types: - HiddenConstructor - HiddenType - Id - - Eq - Synonym - Maybe +Local Classes: + - Eq + Class Members: - Eq.eq diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportExplicit.snap index 55c4666c..33e0630c 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportExplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportExplicit @@ -12,6 +13,8 @@ Terms: Types: - Id is Explicit +Classes: + Terms: - Id is Implicit @@ -19,6 +22,8 @@ Types: - Hidden is Implicit - Id is Implicit +Classes: + Qualified Imports: QualifiedExplicit Terms: @@ -27,6 +32,8 @@ QualifiedExplicit Terms: QualifiedExplicit Types: - Id is Explicit +QualifiedExplicit Classes: + QualifiedImplicit Terms: - Id is Implicit @@ -34,14 +41,20 @@ QualifiedImplicit Types: - Hidden is Implicit - Id is Implicit +QualifiedImplicit Classes: + Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportForLocalOnly.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportForLocalOnly.snap index c75a43e4..65a31233 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportForLocalOnly.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportForLocalOnly.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportForLocalOnly @@ -11,12 +12,16 @@ Terms: Types: +Classes: + Terms: - Id is Explicit Types: - Id is Explicit +Classes: + Qualified Imports: Exported Terms: @@ -25,10 +30,14 @@ Exported Terms: Exported Types: - Id +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportHiddenConstructor.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportHiddenConstructor.snap index 5c4ab3f6..ecdfa04a 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportHiddenConstructor.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportHiddenConstructor.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportHiddenConstructor @@ -11,16 +12,22 @@ Terms: Types: - Hidden is Explicit +Classes: + Qualified Imports: Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap index 50fc4a36..687e41bf 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportQualifiedExplicit @@ -19,6 +20,8 @@ L Types: - MultiTy is Explicit - TypeOnly is Explicit +L Classes: + Exported Terms: - libFn - MkLibTy @@ -30,10 +33,14 @@ Exported Types: - MultiTy - TypeOnly +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap index 0da4f753..435d4e2a 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportQualifiedHiding @@ -18,6 +19,8 @@ L Types: - LibTy is Implicit - TypeOnly is Implicit +L Classes: + Exported Terms: - libFn - MkLibTy @@ -28,10 +31,14 @@ Exported Types: - LibTy - TypeOnly +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap index 27492199..ac8d6f2e 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportQualifiedImplicit @@ -23,6 +24,8 @@ L Types: - TypeOnly is Implicit - HideTy is Implicit +L Classes: + Exported Terms: - MkHideTy - Ctor3 @@ -38,10 +41,14 @@ Exported Types: - TypeOnly - HideTy +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap index 30595e6a..ad768136 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportUnqualifiedExplicit @@ -17,6 +18,8 @@ Types: - MultiTy is Explicit - TypeOnly is Explicit +Classes: + Qualified Imports: Exported Terms: @@ -30,10 +33,14 @@ Exported Types: - MultiTy - TypeOnly +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap index 8a35beca..bdfabdb3 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportUnqualifiedHiding @@ -16,6 +17,8 @@ Types: - LibTy is Implicit - TypeOnly is Implicit +Classes: + Qualified Imports: Exported Terms: @@ -28,10 +31,14 @@ Exported Types: - LibTy - TypeOnly +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap index 5fe83dfa..78556a04 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportUnqualifiedImplicit @@ -21,6 +22,8 @@ Types: - TypeOnly is Implicit - HideTy is Implicit +Classes: + Qualified Imports: Exported Terms: @@ -38,10 +41,14 @@ Exported Types: - TypeOnly - HideTy +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/Library.snap b/tests-integration/fixtures/resolving/002_import_resolution/Library.snap index 23f9de74..81a3fa27 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/Library.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/Library.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Library @@ -23,6 +24,8 @@ Exported Types: - TypeOnly - HideTy +Exported Classes: + Local Terms: - MkHideTy - Ctor3 @@ -38,6 +41,8 @@ Local Types: - TypeOnly - HideTy +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/LibraryExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/LibraryExplicit.snap index 2b72b3e1..8784fb7a 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/LibraryExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/LibraryExplicit.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module LibraryExplicit @@ -15,6 +16,8 @@ Exported Types: - Hidden - Id +Exported Classes: + Local Terms: - Hidden - Id @@ -23,6 +26,8 @@ Local Types: - Hidden - Id +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/DuplicateLocal.snap b/tests-integration/fixtures/resolving/003_import_errors/DuplicateLocal.snap index 50e1f5ab..374a33e3 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/DuplicateLocal.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/DuplicateLocal.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module DuplicateLocal @@ -14,12 +15,16 @@ Exported Terms: Exported Types: - Data +Exported Classes: + Local Terms: - value Local Types: - Data +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap b/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap index 91a1e105..a217552b 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module DuplicateQualifiedImport @@ -15,14 +16,20 @@ Library Types: - Css is Implicit - Html is Implicit +Library Classes: + Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/InvalidConstructor.snap b/tests-integration/fixtures/resolving/003_import_errors/InvalidConstructor.snap index 70a0df0b..f2d5902a 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/InvalidConstructor.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/InvalidConstructor.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module InvalidConstructor @@ -13,11 +14,15 @@ Exported Terms: Exported Types: - Invalid +Exported Classes: + Local Terms: Local Types: - Invalid +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/InvalidImport.snap b/tests-integration/fixtures/resolving/003_import_errors/InvalidImport.snap index e5636e5a..70bb2c49 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/InvalidImport.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/InvalidImport.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module InvalidImport @@ -11,22 +12,30 @@ Terms: Types: - Invalid is Explicit +Classes: + Terms: Types: - Css is Explicit - Html is Explicit +Classes: + Qualified Imports: Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/LibraryA.snap b/tests-integration/fixtures/resolving/003_import_errors/LibraryA.snap index 3cd49974..6a0cdf75 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/LibraryA.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/LibraryA.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module LibraryA @@ -15,6 +16,8 @@ Exported Types: - Css - Html +Exported Classes: + Local Terms: - html @@ -22,6 +25,8 @@ Local Types: - Css - Html +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/003_import_errors/LibraryB.snap b/tests-integration/fixtures/resolving/003_import_errors/LibraryB.snap index dd3ab0e4..f305805f 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/LibraryB.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/LibraryB.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module LibraryB @@ -14,12 +15,16 @@ Exported Terms: Exported Types: - Html +Exported Classes: + Local Terms: - html Local Types: - Html +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Internal.snap b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Internal.snap index 3553a677..2a2a9ca4 100644 --- a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Internal.snap +++ b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Internal.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Internal @@ -14,12 +15,16 @@ Exported Terms: Exported Types: - Internal +Exported Classes: + Local Terms: - Internal Local Types: - Internal +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Library.snap b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Library.snap index 40890244..c97fcbf1 100644 --- a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Library.snap +++ b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Library.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Library @@ -14,16 +15,22 @@ Internal Terms: Internal Types: - Internal is Implicit +Internal Classes: + Exported Terms: - Internal Exported Types: - Internal +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Main.snap b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Main.snap index 7f2fa3ad..8c97f6e8 100644 --- a/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Main.snap +++ b/tests-integration/fixtures/resolving/004_import_re_exported_constructor/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module Main @@ -12,6 +13,8 @@ Terms: Types: - Internal is Explicit +Classes: + Qualified Imports: Exported Terms: @@ -19,11 +22,15 @@ Exported Terms: Exported Types: +Exported Classes: + Local Terms: - internal Local Types: +Local Classes: + Class Members: Errors: diff --git a/tests-integration/fixtures/resolving/005_class_members/ClassLibrary.snap b/tests-integration/fixtures/resolving/005_class_members/ClassLibrary.snap index 7365a8a5..a8f7cb06 100644 --- a/tests-integration/fixtures/resolving/005_class_members/ClassLibrary.snap +++ b/tests-integration/fixtures/resolving/005_class_members/ClassLibrary.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ClassLibrary @@ -15,6 +16,8 @@ Exported Terms: - eq Exported Types: + +Exported Classes: - Show - Ord - Eq @@ -26,6 +29,8 @@ Local Terms: - eq Local Types: + +Local Classes: - Show - Ord - Eq diff --git a/tests-integration/fixtures/resolving/005_class_members/HiddenClass.snap b/tests-integration/fixtures/resolving/005_class_members/HiddenClass.snap index 45f1269a..ae92f1d2 100644 --- a/tests-integration/fixtures/resolving/005_class_members/HiddenClass.snap +++ b/tests-integration/fixtures/resolving/005_class_members/HiddenClass.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module HiddenClass @@ -13,6 +14,8 @@ Terms: - eq is Implicit Types: + +Classes: - Show is Implicit - Ord is Implicit @@ -22,10 +25,14 @@ Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: - Show.show (imported) - Ord.compare (imported) diff --git a/tests-integration/fixtures/resolving/005_class_members/ImportedClass.snap b/tests-integration/fixtures/resolving/005_class_members/ImportedClass.snap index cb86bf63..dabbcfc4 100644 --- a/tests-integration/fixtures/resolving/005_class_members/ImportedClass.snap +++ b/tests-integration/fixtures/resolving/005_class_members/ImportedClass.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ImportedClass @@ -9,6 +10,8 @@ Unqualified Imports: Terms: Types: + +Classes: - Show is Explicit - Eq is Explicit @@ -18,10 +21,14 @@ Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: - Show.show (imported) - Eq.eq (imported) diff --git a/tests-integration/fixtures/resolving/005_class_members/LocalClass.snap b/tests-integration/fixtures/resolving/005_class_members/LocalClass.snap index 3082f572..c2b319b5 100644 --- a/tests-integration/fixtures/resolving/005_class_members/LocalClass.snap +++ b/tests-integration/fixtures/resolving/005_class_members/LocalClass.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module LocalClass @@ -13,6 +14,8 @@ Exported Terms: - apply Exported Types: + +Exported Classes: - Apply - Functor @@ -21,6 +24,8 @@ Local Terms: - apply Local Types: + +Local Classes: - Apply - Functor diff --git a/tests-integration/fixtures/resolving/005_class_members/ReExportConsumer.snap b/tests-integration/fixtures/resolving/005_class_members/ReExportConsumer.snap index cff59af0..9c896fd4 100644 --- a/tests-integration/fixtures/resolving/005_class_members/ReExportConsumer.snap +++ b/tests-integration/fixtures/resolving/005_class_members/ReExportConsumer.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ReExportConsumer @@ -9,6 +10,8 @@ Unqualified Imports: Terms: Types: + +Classes: - Show is Explicit - Eq is Explicit @@ -18,10 +21,14 @@ Exported Terms: Exported Types: +Exported Classes: + Local Terms: Local Types: +Local Classes: + Class Members: - Show.show (imported) - Eq.eq (imported) diff --git a/tests-integration/fixtures/resolving/005_class_members/ReExporter.snap b/tests-integration/fixtures/resolving/005_class_members/ReExporter.snap index 0a005942..95c06ea1 100644 --- a/tests-integration/fixtures/resolving/005_class_members/ReExporter.snap +++ b/tests-integration/fixtures/resolving/005_class_members/ReExporter.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/resolving/generated.rs +assertion_line: 12 expression: report --- module ReExporter @@ -13,6 +14,8 @@ Terms: - eq is Implicit Types: + +Classes: - Show is Implicit - Ord is Implicit - Eq is Implicit @@ -26,6 +29,8 @@ Exported Terms: - eq Exported Types: + +Exported Classes: - Show - Ord - Eq @@ -34,6 +39,8 @@ Local Terms: Local Types: +Local Classes: + Class Members: - Show.show (imported) - Eq.eq (imported) diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 3c227c35..9edef9d8 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -38,6 +38,15 @@ pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { } writeln!(buffer, " - {name} is {kind:?}").unwrap(); } + + writeln!(buffer).unwrap(); + writeln!(buffer, "Classes:").unwrap(); + for (name, _, _, kind) in import.iter_classes() { + if matches!(kind, ImportKind::Hidden) { + continue; + } + writeln!(buffer, " - {name} is {kind:?}").unwrap(); + } } writeln!(buffer).unwrap(); @@ -60,6 +69,15 @@ pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { } writeln!(buffer, " - {name} is {kind:?}").unwrap(); } + + writeln!(buffer).unwrap(); + writeln!(buffer, "{name} Classes:").unwrap(); + for (name, _, _, kind) in import.iter_classes() { + if matches!(kind, ImportKind::Hidden) { + continue; + } + writeln!(buffer, " - {name} is {kind:?}").unwrap(); + } } writeln!(buffer).unwrap(); @@ -74,6 +92,12 @@ pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { writeln!(buffer, " - {name}").unwrap(); } + writeln!(buffer).unwrap(); + writeln!(buffer, "Exported Classes:").unwrap(); + for (name, _, _) in resolved.exports.iter_classes() { + writeln!(buffer, " - {name}").unwrap(); + } + writeln!(buffer).unwrap(); writeln!(buffer, "Local Terms:").unwrap(); for (name, _, _) in resolved.locals.iter_terms() { @@ -86,6 +110,12 @@ pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { writeln!(buffer, " - {name}").unwrap(); } + writeln!(buffer).unwrap(); + writeln!(buffer, "Local Classes:").unwrap(); + for (name, _, _) in resolved.locals.iter_classes() { + writeln!(buffer, " - {name}").unwrap(); + } + writeln!(buffer).unwrap(); writeln!(buffer, "Class Members:").unwrap(); let indexed = engine.indexed(id).unwrap(); From 4c1c4c48eb2715c75facbb51af657e2befc71caa Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 20:20:20 +0800 Subject: [PATCH 130/386] Implement implication-based constraint solving This adds implication-based constraint solving to replace the local constraint solving that was being done in let binding groups. This potentially enables future extensions to the language like GADTs. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: Amp --- .../checking/src/algorithm/constraint.rs | 69 +++++++++++++++- .../checking/src/algorithm/derive.rs | 12 +-- .../checking/src/algorithm/derive/tools.rs | 4 +- .../checking/src/algorithm/inspect.rs | 2 +- .../checking/src/algorithm/kind/synonym.rs | 3 +- compiler-core/checking/src/algorithm/state.rs | 79 +++++++++---------- .../src/algorithm/state/implication.rs | 79 +++++++++++++++++++ compiler-core/checking/src/algorithm/term.rs | 28 ++----- .../checking/src/algorithm/term_item.rs | 20 ++--- .../checking/src/algorithm/toolkit.rs | 4 +- .../checking/src/algorithm/unification.rs | 2 +- 11 files changed, 216 insertions(+), 86 deletions(-) create mode 100644 compiler-core/checking/src/algorithm/state/implication.rs diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 9dc60bc0..7058555b 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -8,8 +8,8 @@ use compiler_solved::*; use functional_dependency::{Fd, get_all_determined}; use std::collections::{HashSet, VecDeque}; -use std::iter; use std::sync::Arc; +use std::{iter, mem}; use building_types::QueryResult; use files::FileId; @@ -18,6 +18,7 @@ use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; use crate::algorithm::fold::{FoldAction, TypeFold, fold_type}; +use crate::algorithm::state::implication::ImplicationId; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::visit::{ CollectFileReferences, HasLabeledRole, TypeVisitor, VisitAction, visit_type, @@ -121,6 +122,72 @@ where Ok(residual) } +#[tracing::instrument(skip_all, name = "solve_implication")] +pub fn solve_implication( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let implication = state.implications.current(); + solve_implication_id(state, context, implication, &[]) +} + +/// Recursively solves an implication and its children. +fn solve_implication_id( + state: &mut CheckState, + context: &CheckContext, + implication: ImplicationId, + inherited: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let (wanted, given, children) = { + let node = &mut state.implications[implication]; + (mem::take(&mut node.wanted), mem::take(&mut node.given), node.children.clone()) + }; + + let all_given = { + let inherited = inherited.iter().copied(); + let given = given.iter().copied(); + inherited.chain(given).collect_vec() + }; + + crate::debug_fields!(state, context, { + ?implication = implication, + ?wanted = wanted.len(), + ?given = given.len(), + ?inherited = inherited.len(), + ?children = children.len(), + }); + + // Solve this implication's children with all_given. + for child in &children { + let residual = solve_implication_id(state, context, *child, &all_given)?; + + crate::debug_fields!(state, context, { + ?child = child, + ?residual = residual.len(), + }); + + // TODO: partition_by_skolem_escape once skolems are introduced. + state.implications[implication].wanted.extend(residual); + } + + // Solve this implication's wanted constraints with all_given. + let remaining = mem::take(&mut state.implications[implication].wanted); + let wanted: VecDeque = wanted.into_iter().chain(remaining).collect(); + let residuals = solve_constraints(state, context, wanted, &all_given)?; + + let implication = &mut state.implications[implication]; + implication.given = given; + implication.wanted = Vec::clone(&residuals).into(); + + Ok(residuals) +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct ConstraintApplication { pub(crate) file_id: FileId, diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index 14501c74..b213b1cc 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -94,7 +94,7 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { - state.with_local_givens(|state| check_derive_head_core(state, context, input)) + state.with_implication(|state| check_derive_head_core(state, context, input)) }) } @@ -297,7 +297,7 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(result.item_id), |state| { - state.with_local_givens(|state| check_derive_member_core(state, context, result)) + state.with_implication(|state| check_derive_member_core(state, context, result)) }) } @@ -310,7 +310,7 @@ where Q: ExternalQueries, { for &constraint in &result.constraints { - state.constraints.push_given(constraint); + state.push_given(constraint); } match &result.strategy { @@ -359,7 +359,7 @@ where generate_delegate_constraint(state, context.prim.t, *derived_type, *class); } DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint } => { - state.constraints.push_wanted(*delegate_constraint); + state.push_wanted(*delegate_constraint); } DeriveStrategy::HeadOnly => { tools::emit_superclass_constraints( @@ -393,10 +393,10 @@ fn generate_delegate_constraint( let class_type = state.storage.intern(Type::Constructor(class.0, class.1)); let given_constraint = state.storage.intern(Type::Application(class_type, skolem_type)); - state.constraints.push_given(given_constraint); + state.push_given(given_constraint); let wanted_constraint = state.storage.intern(Type::Application(class_type, applied_type)); - state.constraints.push_wanted(wanted_constraint); + state.push_wanted(wanted_constraint); } fn try_peel_trailing_skolems( diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index a3cf5726..058c77f8 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -34,7 +34,7 @@ pub fn emit_constraint( ) { let class_type = state.storage.intern(Type::Constructor(class_file, class_id)); let constraint = state.storage.intern(Type::Application(class_type, type_id)); - state.constraints.push_wanted(constraint); + state.push_wanted(constraint); } /// Emits wanted constraints for the superclasses of the class being derived. @@ -73,7 +73,7 @@ where for &(superclass, _) in class_info.superclasses.iter() { let specialised = substitute::SubstituteBindings::on(state, &bindings, superclass); - state.constraints.push_wanted(specialised); + state.push_wanted(specialised); } Ok(()) diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index 52522349..a4827ed3 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -107,7 +107,7 @@ where } Type::Constrained(constraint, constrained) => { - state.constraints.push_given(constraint); + state.push_given(constraint); current_id = constrained; } diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 3d99e034..b73e2674 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -16,8 +16,7 @@ use lowering::{GroupedModule, LoweredModule, TypeItemIr}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, substitute, transfer, unification}; -use crate::core::debruijn; -use crate::core::{Saturation, Synonym, Type, TypeId}; +use crate::core::{Saturation, Synonym, Type, TypeId, debruijn}; use crate::error::ErrorKind; use crate::{CheckedModule, ExternalQueries}; diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 50514252..8eca5adb 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1,14 +1,15 @@ pub mod unification; -use itertools::Itertools; pub use unification::*; -use std::collections::VecDeque; -use std::mem; +pub mod implication; +pub use implication::*; + use std::sync::Arc; use building_types::QueryResult; use files::FileId; use indexing::{IndexedModule, TermItemId, TypeItemId}; +use itertools::Itertools; use lowering::{ BinderId, GraphNodeId, GroupedModule, ImplicitBindingId, LetBindingNameGroupId, LoweredModule, RecordPunId, TermOperatorId, TypeItemIr, TypeOperatorId, TypeVariableBindingId, @@ -264,31 +265,6 @@ impl SurfaceBindings { } } -/// Collects wanted and given constraints. -#[derive(Default)] -pub struct ConstraintContext { - pub wanted: VecDeque, - pub given: Vec, -} - -impl ConstraintContext { - pub fn push_wanted(&mut self, constraint: TypeId) { - self.wanted.push_back(constraint); - } - - pub fn extend_wanted(&mut self, constraints: &[TypeId]) { - self.wanted.extend(constraints); - } - - pub fn push_given(&mut self, constraint: TypeId) { - self.given.push(constraint); - } - - pub fn take(&mut self) -> (VecDeque, Vec) { - (mem::take(&mut self.wanted), mem::take(&mut self.given)) - } -} - /// The core state structure threaded through the [`algorithm`]. /// /// [`algorithm`]: crate::algorithm @@ -307,8 +283,9 @@ pub struct CheckState { /// Tracks surface variables for rebinding, see struct documentation. pub surface_bindings: SurfaceBindings, - /// Collects wanted/given type class constraints. - pub constraints: ConstraintContext, + /// Stores implication scopes for constraint solving. + pub implications: Implications, + /// Collects unification variables and solutions. pub unification: UnificationContext, /// The in-progress binding group; used for recursive declarations. @@ -1043,24 +1020,46 @@ impl CheckState { result } - pub fn with_local_givens(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { - let length = self.constraints.given.len(); + pub fn with_implication(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { + let child = self.push_implication(); let result = action(self); - self.constraints.given.drain(length..); + self.pop_implication(child); result } + pub fn push_implication(&mut self) -> ImplicationId { + self.implications.push() + } + + pub fn pop_implication(&mut self, implication: ImplicationId) { + self.implications.pop(implication); + } + + pub fn current_implication(&self) -> ImplicationId { + self.implications.current() + } + + pub fn current_implication_mut(&mut self) -> &mut implication::Implication { + self.implications.current_mut() + } + + pub fn push_wanted(&mut self, constraint: TypeId) { + self.current_implication_mut().wanted.push_back(constraint); + } + + pub fn extend_wanted(&mut self, constraints: &[TypeId]) { + self.current_implication_mut().wanted.extend(constraints.iter().copied()); + } + + pub fn push_given(&mut self, constraint: TypeId) { + self.current_implication_mut().given.push(constraint); + } + pub fn solve_constraints(&mut self, context: &CheckContext) -> QueryResult> where Q: ExternalQueries, { - let (wanted, given) = self.constraints.take(); - let residuals = constraint::solve_constraints(self, context, wanted, &given); - - let after_solve = mem::replace(&mut self.constraints.given, given); - debug_assert!(after_solve.is_empty(), "invariant violated: non-empty givens"); - - residuals + constraint::solve_implication(self, context) } pub fn report_exhaustiveness(&mut self, exhaustiveness: ExhaustivenessReport) { diff --git a/compiler-core/checking/src/algorithm/state/implication.rs b/compiler-core/checking/src/algorithm/state/implication.rs new file mode 100644 index 00000000..6c940ae6 --- /dev/null +++ b/compiler-core/checking/src/algorithm/state/implication.rs @@ -0,0 +1,79 @@ +use std::collections::VecDeque; +use std::ops::{Index, IndexMut}; + +use crate::core::{TypeId, debruijn}; + +/// A unique identifier for an implication scope. +pub type ImplicationId = u32; + +/// A node in the implication tree. +#[derive(Default)] +pub struct Implication { + pub skolems: Vec, + pub given: Vec, + pub wanted: VecDeque, + pub children: Vec, + pub parent: Option, +} + +impl Implication { + pub fn new(parent: Option) -> Implication { + Implication { parent, ..Implication::default() } + } +} + +pub struct Implications { + nodes: Vec, + current: ImplicationId, +} + +impl Implications { + pub fn new() -> Self { + Implications { nodes: vec![Implication::new(None)], current: 0 } + } + + pub fn current(&self) -> ImplicationId { + self.current + } + + pub fn current_mut(&mut self) -> &mut Implication { + let current = self.current as usize; + &mut self.nodes[current] + } + + pub fn push(&mut self) -> ImplicationId { + let parent = self.current; + let id = self.nodes.len() as ImplicationId; + self.nodes.push(Implication::new(Some(parent))); + self.nodes[parent as usize].children.push(id); + self.current = id; + id + } + + pub fn pop(&mut self, implication: ImplicationId) { + debug_assert_eq!(implication, self.current); + let parent = + self[implication].parent.expect("invariant violated: missing implication parent"); + self.current = parent; + } +} + +impl Default for Implications { + fn default() -> Implications { + Implications::new() + } +} + +impl Index for Implications { + type Output = Implication; + + fn index(&self, index: ImplicationId) -> &Self::Output { + &self.nodes[index as usize] + } +} + +impl IndexMut for Implications { + fn index_mut(&mut self, index: ImplicationId) -> &mut Self::Output { + &mut self.nodes[index as usize] + } +} diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 1a3c7a82..7395ca17 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1,5 +1,4 @@ use std::iter; -use std::mem; use building_types::QueryResult; use itertools::{Itertools, Position}; @@ -1605,7 +1604,7 @@ where // Constraint generation, like `toolkit::collect_constraints` Type::Constrained(constraint, constrained) => { - state.constraints.push_wanted(constraint); + state.push_wanted(constraint); check_function_application_core( state, context, @@ -1750,19 +1749,11 @@ fn check_let_name_binding( where Q: ExternalQueries, { - let outer_wanted = mem::take(&mut state.constraints.wanted); - let outer_given = mem::take(&mut state.constraints.given); - - let result = state.with_error_step(ErrorStep::CheckingLetName(id), |state| { - check_let_name_binding_core(state, context, id) - }); - - // Residuals from solving are the only wanteds remaining. - let residual = mem::replace(&mut state.constraints.wanted, outer_wanted); - state.constraints.given = outer_given; - state.constraints.wanted.extend(residual); - - result + state.with_implication(|state| { + state.with_error_step(ErrorStep::CheckingLetName(id), |state| { + check_let_name_binding_core(state, context, id) + }) + }) } fn check_let_name_binding_core( @@ -1802,13 +1793,6 @@ where equation::patterns(state, context, origin, &name.equations)?; } - // PureScript does not generalise let bindings. Constraints propagate - // to the enclosing declaration where they are solved via dictionary - // passing. Both wanteds and givens are scoped to this binding by - // the caller, so the standard solver only sees local constraints. - let residual = equation::constraints(state, context, equation::ConstraintsPolicy::Return)?; - state.constraints.extend_wanted(&residual); - Ok(()) } diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 95f8b6ed..fce6d387 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -8,6 +8,7 @@ use lowering::TermItemIr; use crate::ExternalQueries; use crate::algorithm::kind::synonym; +use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; use crate::algorithm::{ constraint, equation, inspect, kind, quantify, substitute, term, transfer, unification, @@ -273,7 +274,7 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { - state.with_local_givens(|state| { + state.with_implication(|state| { let _span = tracing::debug_span!("check_value_group").entered(); check_value_group_core(context, state, input) }) @@ -463,7 +464,7 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(input.instance_id), |state| { - state.with_local_givens(|state| check_instance_member_group_core(state, context, input)) + state.with_implication(|state| check_instance_member_group_core(state, context, input)) }) } @@ -505,7 +506,7 @@ where for (constraint_type, _) in instance_constraints { let local_constraint = transfer::localize(state, context, *constraint_type); - state.constraints.push_given(local_constraint); + state.push_given(local_constraint); } let specialised_type = if let Some(class_member_type) = class_member_type { @@ -523,13 +524,14 @@ where // The specialised type may have constraints like `Show a => (a -> b) -> f a -> f b`. // We push `Show a` as a given and use the body `(a -> b) -> f a -> f b` for checking. let specialised_type = specialised_type.map(|mut t| { - while let normalized = state.normalize_type(t) - && let Type::Constrained(constraint, constrained) = &state.storage[normalized] - { - state.constraints.push_given(*constraint); - t = *constrained; + safe_loop! { + let normalized = state.normalize_type(t); + let Type::Constrained(constraint, constrained) = state.storage[normalized] else { + break t; + }; + state.push_given(constraint); + t = constrained; } - t }); if let Some(signature_id) = &member.signature { diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index c2080422..301adff1 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -138,7 +138,7 @@ pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeI safe_loop! { type_id = state.normalize_type(type_id); if let Type::Constrained(constraint, constrained) = state.storage[type_id] { - state.constraints.push_wanted(constraint); + state.push_wanted(constraint); type_id = constrained; } else { break type_id; @@ -156,7 +156,7 @@ pub fn collect_given_constraints(state: &mut CheckState, mut type_id: TypeId) -> safe_loop! { type_id = state.normalize_type(type_id); if let Type::Constrained(constraint, constrained) = state.storage[type_id] { - state.constraints.push_given(constraint); + state.push_given(constraint); type_id = constrained; } else { break type_id; diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 78e54594..e5982e92 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -137,7 +137,7 @@ where } (Type::Constrained(constraint, inner), _) if mode == ElaborationMode::Yes => { - state.constraints.push_wanted(constraint); + state.push_wanted(constraint); subtype_with_mode(state, context, inner, t2, mode) } From 6a80c1df6de15ca7225fe924033d0eb33bff505d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 20:26:21 +0800 Subject: [PATCH 131/386] Update inline documentation --- compiler-core/checking/src/algorithm/constraint.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 7058555b..9dfd7d02 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -900,8 +900,8 @@ fn can_unify(state: &mut CheckState, t1: TypeId, t2: TypeId) -> CanUnify { .and_also(|| can_unify(state, t1_result, t2_result)) } - // Function(a, b) and Application(Application(head, a), b) can potentially unify - // when head resolves to the Function constructor. + // Function(a, b) and Application(Application(f, a), b) can + // unify when `f` resolves to the Function constructor. (&Type::Function(..), &Type::Application(..)) | (&Type::Application(..), &Type::Function(..)) => Unify, @@ -1101,10 +1101,9 @@ where } } - // Given constraints are valid by construction (from signatures and - // superclass elaboration). When a unification variable makes a position - // stuck, it is safe to emit an equality rather than requiring functional - // dependencies to cover it — the given constraint is authoritative. + // Given constraints are valid by construction. When a unification + // variable makes a position stuck, it's safe to emit an equality + // rather than require functional dependencies to cover it. let equalities = stuck_positions.iter().map(|&index| { let wanted = wanted.arguments[index]; let given = given.arguments[index]; From 0793f140afafa33432e636d79dfce1068009f426 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Feb 2026 20:51:06 +0800 Subject: [PATCH 132/386] Handle type synonyms whose result kind is a function kind When a type synonym's kind signature has more function arrows than the definition has parameters, the excess arrows belong to the result kind. Previously, both check_signature_like and infer_synonym_application required an exact match between arrow count and parameter count, which rejected these synonyms with TypeSignatureVariableMismatch and PartialSynonymApplication errors respectively. The fix folds excess kind arrows back into the result kind during signature checking, and applies excess arguments as regular type applications after synonym expansion. See test 309 for examples. Co-Authored-By: Claude Opus 4.6 --- .../checking/src/algorithm/kind/synonym.rs | 48 +++++++++++++++++-- .../checking/src/algorithm/type_item.rs | 25 ++++++++-- .../Main.purs | 9 ++++ .../Main.snap | 30 ++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.purs create mode 100644 tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.snap diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index b73e2674..2fd0cd25 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -179,7 +179,21 @@ where let expected_arity = synonym.type_variables.0 as usize; let actual_arity = arguments.len(); - if expected_arity != actual_arity { + // A synonym's result kind can itself be a function kind, which means + // the application chain may contain more arguments than the synonym + // has parameters. For example: + // + // type C2 :: forall k. (k -> Type) -> (k -> Type) -> k -> Type + // type C2 a z = Coproduct a z + // + // in1 :: forall a z. a ~> C2 a z + // + // The lowered application chain for `C2 a z x` has 3 arguments, but + // the synonym only has 2 parameters. After expanding with the first + // 2 arguments, `C2 a z` becomes `Coproduct a z` with kind `k -> Type`. + // The third argument `x` is then applied to the expanded result as a + // regular type application, giving `Coproduct a z x` of kind `Type`. + if actual_arity < expected_arity { if state.defer_synonym_expansion { let (synonym_type, synonym_kind) = infer_partial_synonym_application( state, @@ -195,13 +209,37 @@ where } } - let defer_synonym_expansion = mem::replace(&mut state.defer_synonym_expansion, true); - - let (synonym_type, synonym_kind) = - infer_synonym_application_arguments(state, context, (file_id, type_id), kind, arguments)?; + // Continuing our previous example, `C2 a z x` produces: + // + // synonym_arguments := [a, z] + // excess_arguments := [x] + let (synonym_arguments, excess_arguments) = arguments.split_at(expected_arity); + let defer_synonym_expansion = mem::replace(&mut state.defer_synonym_expansion, true); + let (mut synonym_type, mut synonym_kind) = infer_synonym_application_arguments( + state, + context, + (file_id, type_id), + kind, + synonym_arguments, + )?; state.defer_synonym_expansion = defer_synonym_expansion; + // Continuing our previous example, `C2 a z x` expands: + // + // synonym_type := Coproduct a z + // synonym_kind := (k -> Type) + // + // Finally, we append the excess arguments to get: + // + // synonym_type := Coproduct a z x + // synonym_kind := Type + // + for &argument in excess_arguments { + (synonym_type, synonym_kind) = + kind::infer_surface_app_kind(state, context, (synonym_type, synonym_kind), argument)?; + } + Ok((synonym_type, synonym_kind)) } diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 853a4158..291463fc 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -699,7 +699,16 @@ where let signature = inspect::inspect_signature(state, context, stored_kind, surface_bindings)?; - if variables.len() != signature.arguments.len() { + // The kind signature may have more function arrows than the + // definition has parameters when the result kind is itself a + // function kind. For example: + // + // type C2 :: forall k. (k -> Type) -> (k -> Type) -> k -> Type + // type C2 a z = Coproduct a z + // + // The kind decomposes into 3 arrows but the synonym only has 2 + // parameters. The excess arrows belong to the result kind. + if variables.len() > signature.arguments.len() { state.insert_error(ErrorKind::TypeSignatureVariableMismatch { id: signature_id, expected: 0, @@ -713,11 +722,13 @@ where return Ok(None); }; - let variables = variables.iter(); - let arguments = signature.arguments.iter(); + let parameter_count = variables.len(); + let (matched_arguments, excess_arguments) = + signature.arguments.split_at(parameter_count); let kinds = variables - .zip(arguments) + .iter() + .zip(matched_arguments.iter()) .map(|(variable, &argument)| { // Use contravariant subtyping for type variables: // @@ -746,7 +757,11 @@ where .collect::>>()?; let kind_variables = signature.variables; - let result_kind = signature.result; + + // Fold excess arguments back into the result kind. + let result_kind = excess_arguments.iter().rfold(signature.result, |result, &argument| { + state.storage.intern(Type::Function(argument, result)) + }); let type_variables = kinds.into_iter().map(|(id, visible, name, kind)| { let level = state.type_scope.bind_forall(id, kind); ForallBinder { visible, implicit: false, name, level, kind } diff --git a/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.purs b/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.purs new file mode 100644 index 00000000..7b0132c9 --- /dev/null +++ b/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Box (f :: Type -> Type) (a :: Type) = Box (f a) + +type Wrap :: (Type -> Type) -> Type -> Type +type Wrap f = Box f + +test :: forall f. Wrap f Int -> Wrap f Int +test x = x diff --git a/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.snap b/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.snap new file mode 100644 index 00000000..6d4c97e1 --- /dev/null +++ b/tests-integration/fixtures/checking/309_synonym_function_result_kind/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: + forall (f :: Type -> Type) (a :: Type). + (f :: Type -> Type) (a :: Type) -> Box (f :: Type -> Type) (a :: Type) +test :: forall (f :: Type -> Type). Wrap (f :: Type -> Type) Int -> Wrap (f :: Type -> Type) Int + +Types +Box :: (Type -> Type) -> Type -> Type +Wrap :: (Type -> Type) -> Type -> Type + +Synonyms +Wrap = forall (f :: Type -> Type). Box (f :: Type -> Type) + Quantified = :0 + Kind = :0 + Type = :1 + + +Data +Box + Quantified = :0 + Kind = :0 + + +Roles +Box = [Representational, Nominal] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index ff1660aa..f22c0977 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -629,3 +629,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_307_where_let_interaction_main() { run_test("307_where_let_interaction", "Main"); } #[rustfmt::skip] #[test] fn test_308_let_constraint_scoping_main() { run_test("308_let_constraint_scoping", "Main"); } + +#[rustfmt::skip] #[test] fn test_309_synonym_function_result_kind_main() { run_test("309_synonym_function_result_kind", "Main"); } From 709b0581c23833c52b6dd523dee53d0ec200a52e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Feb 2026 01:04:50 +0800 Subject: [PATCH 133/386] Use batch substitution in inspection The previous implementation was susceptible to level conflicts because of synonym expansion, which can introduce variables while we're also performing substitutions. This commit refactors the algorithm to only perform substitution once we encounter a non-Forall type post-expansion. --- .../checking/src/algorithm/inspect.rs | 134 +++++++++++------- .../310_synonym_forall_expansion/Main.purs | 6 + .../310_synonym_forall_expansion/Main.snap | 22 +++ tests-integration/tests/checking/generated.rs | 2 + 4 files changed, 111 insertions(+), 53 deletions(-) create mode 100644 tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.purs create mode 100644 tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.snap diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index a4827ed3..51157979 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -1,4 +1,3 @@ -use std::mem; use std::sync::Arc; use building_types::QueryResult; @@ -6,6 +5,7 @@ use lowering::TypeVariableBindingId; use crate::ExternalQueries; use crate::algorithm::kind::synonym; +use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::substitute; use crate::core::{ForallBinder, Type, TypeId, Variable, debruijn}; @@ -63,46 +63,71 @@ pub fn inspect_signature( where Q: ExternalQueries, { - const INSPECTION_LIMIT: u32 = 1_000_000; - + // Consider a synonym that hides a quantifier: + // + // type NaturalTransformation f g = forall a. f a -> g a + // + // transform :: forall f g. NaturalTransformation f g + // + // With De Bruijn levels, `transform`'s type becomes: + // + // transform :: forall f:0 g:1. NaturalTransformation f g + // + // Synonym expansion can reveal additional quantifiers: + // + // transform :: forall f:0 g:1. forall a:2. f a -> g a + // + // The following algorithm is designed to handle level + // adjustments relative to the current scope level. let mut surface_bindings = surface_bindings.iter(); let mut variables = vec![]; let mut current_id = type_id; - for _ in 0..INSPECTION_LIMIT { - let expanded_id = synonym::normalize_expand_type(state, context, current_id)?; - match state.storage[expanded_id] { - // Foralls can and will have levels that are different from - // when they were originally checked, such as when a Forall - // appears on a type synonym. We rebind each type variable - // to get the current level then substitute it within the - // quantified type to ensure correct scoping. + let mut bindings = substitute::LevelToType::default(); + + safe_loop! { + current_id = synonym::normalize_expand_type(state, context, current_id)?; + + // In the example, after the Forall case has peeled f:0, g:1, and the + // synonym-expanded a:2, the accumulated bindings are the following: + // + // { 0 -> f', 1 -> g', 2 -> 'a } + // + // We're at a monomorphic type at this point, f:0 a:2 -> g:1 a:2, so + // we can now proceed with applying the substitutions and continuing. + if !matches!(state.storage[current_id], Type::Forall(..)) && !bindings.is_empty() { + current_id = substitute::SubstituteBindings::on(state, &bindings, current_id); + bindings.clear(); + continue; + } + + match state.storage[current_id] { + // Bind each ForallBinder relative to the current scope, recording the + // level substitution for later. In the example, this accumulates the + // following substitutions before we hit the Function type: + // + // { 0 -> f', 1 -> g', 2 -> 'a } + // Type::Forall(ref binder, inner) => { - let mut binder = binder.clone(); - - // Only consume a surface binding for explicit foralls; - // implicit foralls are added during generalisation and - // don't have corresponding surface bindings. - let new_level = if !binder.implicit { - if let Some(&binding_id) = surface_bindings.next() { - state.type_scope.bound.bind(debruijn::Variable::Forall(binding_id)) - } else { - state.type_scope.bound.bind(debruijn::Variable::Core) - } + let mut binder = ForallBinder::clone(binder); + + let new_level = if !binder.implicit + && let Some(&binding_id) = surface_bindings.next() + { + state.type_scope.bound.bind(debruijn::Variable::Forall(binding_id)) } else { state.type_scope.bound.bind(debruijn::Variable::Core) }; - let old_level = mem::replace(&mut binder.level, new_level); - + let old_level = binder.level; + binder.level = new_level; state.type_scope.kinds.insert(new_level, binder.kind); - let variable = - state.storage.intern(Type::Variable(Variable::Bound(new_level, binder.kind))); - let inner = substitute::SubstituteBound::on(state, old_level, variable, inner); + let variable = Type::Variable(Variable::Bound(new_level, binder.kind)); + let variable = state.storage.intern(variable); + bindings.insert(old_level, variable); variables.push(binder); - current_id = inner; } @@ -112,17 +137,14 @@ where } _ => { - let function = expanded_id; - let (arguments, result) = signature_components_core(state, context, expanded_id)?; - return Ok(InspectSignature { variables, function, arguments, result }); + let (arguments, result) = signature_components(state, context, current_id)?; + return Ok(InspectSignature { variables, function: current_id, arguments, result }); } } } - - unreachable!("critical violation: limit reached in inspect_signature_core") } -fn signature_components_core( +fn signature_components( state: &mut CheckState, context: &CheckContext, type_id: TypeId, @@ -130,36 +152,42 @@ fn signature_components_core( where Q: ExternalQueries, { - const INSPECTION_LIMIT: u32 = 1_000_000; - let mut arguments = vec![]; let mut current_id = type_id; + let mut bindings = substitute::LevelToType::default(); - for _ in 0..INSPECTION_LIMIT { - let expanded = synonym::normalize_expand_type(state, context, current_id)?; - match state.storage[expanded] { - Type::Function(argument_id, return_id) => { - arguments.push(argument_id); - current_id = return_id; - } + safe_loop! { + current_id = synonym::normalize_expand_type(state, context, current_id)?; + + if !matches!(state.storage[current_id], Type::Forall(..)) && !bindings.is_empty() { + current_id = substitute::SubstituteBindings::on(state, &bindings, current_id); + bindings.clear(); + continue; + } + match state.storage[current_id] { Type::Forall(ref binder, inner) => { - let binder_level = binder.level; - let binder_kind = binder.kind; + let old_level = binder.level; + let kind = binder.kind; - let level = state.type_scope.bound.bind(debruijn::Variable::Core); - state.type_scope.kinds.insert(level, binder_kind); + let new_level = state.type_scope.bound.bind(debruijn::Variable::Core); + state.type_scope.kinds.insert(new_level, kind); - let variable = - state.storage.intern(Type::Variable(Variable::Bound(level, binder_kind))); - current_id = substitute::SubstituteBound::on(state, binder_level, variable, inner); + let variable = Type::Variable(Variable::Bound(new_level, kind)); + let variable = state.storage.intern(variable); + + bindings.insert(old_level, variable); + current_id = inner; + } + + Type::Function(argument_id, return_id) => { + arguments.push(argument_id); + current_id = return_id; } _ => { - return Ok((arguments, expanded)); + return Ok((arguments, current_id)); } } } - - unreachable!("critical violation: limit reached in signature_components_core") } diff --git a/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.purs b/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.purs new file mode 100644 index 00000000..9a8c39a5 --- /dev/null +++ b/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.purs @@ -0,0 +1,6 @@ +module Main where + +type NatTrans f g = forall a. f a -> g a + +apply :: forall f g. NatTrans f g -> f Int -> g Int +apply nat fa = nat fa diff --git a/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.snap b/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.snap new file mode 100644 index 00000000..cd745547 --- /dev/null +++ b/tests-integration/fixtures/checking/310_synonym_forall_expansion/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +apply :: + forall (f :: Type -> Type) (g :: Type -> Type). + NatTrans (f :: Type -> Type) (g :: Type -> Type) -> + (f :: Type -> Type) Int -> + (g :: Type -> Type) Int + +Types +NatTrans :: forall (t12 :: Type). ((t12 :: Type) -> Type) -> ((t12 :: Type) -> Type) -> Type + +Synonyms +NatTrans = forall (t12 :: Type) (f :: (t12 :: Type) -> Type) (g :: (t12 :: Type) -> Type) (a :: (t12 :: Type)). + (f :: (t12 :: Type) -> Type) (a :: (t12 :: Type)) -> + (g :: (t12 :: Type) -> Type) (a :: (t12 :: Type)) + Quantified = :1 + Kind = :0 + Type = :2 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index f22c0977..21ce64f9 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -631,3 +631,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_308_let_constraint_scoping_main() { run_test("308_let_constraint_scoping", "Main"); } #[rustfmt::skip] #[test] fn test_309_synonym_function_result_kind_main() { run_test("309_synonym_function_result_kind", "Main"); } + +#[rustfmt::skip] #[test] fn test_310_synonym_forall_expansion_main() { run_test("310_synonym_forall_expansion", "Main"); } From 198082c3c7acfbe01498718c13bb4beab1c7125b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Feb 2026 03:44:02 +0800 Subject: [PATCH 134/386] Implement synthetic lookup for qualified Prim --- .../checking/src/algorithm/type_item.rs | 3 +- compiler-core/resolving/src/lib.rs | 55 ++++++++++++++++--- .../checking/311_prim_qualified/Main.purs | 3 + .../checking/311_prim_qualified/Main.snap | 12 ++++ .../312_prim_qualified_override/Main.purs | 5 ++ .../312_prim_qualified_override/Main.snap | 24 ++++++++ tests-integration/tests/checking/generated.rs | 4 ++ 7 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 tests-integration/fixtures/checking/311_prim_qualified/Main.purs create mode 100644 tests-integration/fixtures/checking/311_prim_qualified/Main.snap create mode 100644 tests-integration/fixtures/checking/312_prim_qualified_override/Main.purs create mode 100644 tests-integration/fixtures/checking/312_prim_qualified_override/Main.snap diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 291463fc..6ffbd9c3 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -723,8 +723,7 @@ where }; let parameter_count = variables.len(); - let (matched_arguments, excess_arguments) = - signature.arguments.split_at(parameter_count); + let (matched_arguments, excess_arguments) = signature.arguments.split_at(parameter_count); let kinds = variables .iter() diff --git a/compiler-core/resolving/src/lib.rs b/compiler-core/resolving/src/lib.rs index 868e4a81..fcb37726 100644 --- a/compiler-core/resolving/src/lib.rs +++ b/compiler-core/resolving/src/lib.rs @@ -62,6 +62,26 @@ pub struct ResolvedModule { } impl ResolvedModule { + fn lookup_qualified( + &self, + qualifier: &str, + lookup: LookupFn, + default: DefaultFn, + ) -> Option<(FileId, ItemId)> + where + LookupFn: FnOnce(&ResolvedImport) -> Option<(FileId, ItemId, ImportKind)>, + DefaultFn: FnOnce() -> Option<(FileId, ItemId)>, + { + if let Some(import) = self.qualified.get(qualifier) { + let (file, id, kind) = lookup(import)?; + if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + } else if qualifier == "Prim" { + default() + } else { + None + } + } + fn lookup_unqualified(&self, lookup: LookupFn) -> Option<(FileId, ItemId)> where LookupFn: Fn(&ResolvedImport) -> Option<(FileId, ItemId, ImportKind)>, @@ -103,9 +123,9 @@ impl ResolvedModule { name: &str, ) -> Option<(FileId, TermItemId)> { if let Some(qualifier) = qualifier { - let import = self.qualified.get(qualifier)?; - let (file, id, kind) = import.lookup_term(name)?; - if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + let lookup_item = |import: &ResolvedImport| import.lookup_term(name); + let lookup_prim = || prim.exports.lookup_term(name); + self.lookup_qualified(qualifier, lookup_item, lookup_prim) } else { let lookup_item = |import: &ResolvedImport| import.lookup_term(name); let lookup_prim = || prim.exports.lookup_term(name); @@ -122,9 +142,9 @@ impl ResolvedModule { name: &str, ) -> Option<(FileId, TypeItemId)> { if let Some(qualifier) = qualifier { - let import = self.qualified.get(qualifier)?; - let (file, id, kind) = import.lookup_type(name)?; - if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + let lookup_item = |import: &ResolvedImport| import.lookup_type(name); + let lookup_prim = || prim.exports.lookup_type(name); + self.lookup_qualified(qualifier, lookup_item, lookup_prim) } else { let lookup_item = |import: &ResolvedImport| import.lookup_type(name); let lookup_prim = || prim.exports.lookup_type(name); @@ -141,9 +161,9 @@ impl ResolvedModule { name: &str, ) -> Option<(FileId, TypeItemId)> { if let Some(qualifier) = qualifier { - let import = self.qualified.get(qualifier)?; - let (file, id, kind) = import.lookup_class(name)?; - if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + let lookup_item = |import: &ResolvedImport| import.lookup_class(name); + let lookup_prim = || prim.exports.lookup_class(name); + self.lookup_qualified(qualifier, lookup_item, lookup_prim) } else { let lookup_item = |import: &ResolvedImport| import.lookup_class(name); let lookup_prim = || prim.exports.lookup_class(name); @@ -185,6 +205,23 @@ impl ResolvedModule { } } + // If an unqualified Prim import exists, use its import list; + if let Some(prim_imports) = self.unqualified.get("Prim") { + for prim_import in prim_imports { + if prim_import.contains_term(file_id, item_id) { + return true; + } + } + } + + // if a qualified Prim import exists, use its import list; + if let Some(prim_import) = self.qualified.get("Prim") + && prim_import.contains_term(file_id, item_id) + { + return true; + } + + // if there are no Prim imports, use the export list. if prim.exports.contains_term(file_id, item_id) { return true; } diff --git a/tests-integration/fixtures/checking/311_prim_qualified/Main.purs b/tests-integration/fixtures/checking/311_prim_qualified/Main.purs new file mode 100644 index 00000000..51cfc931 --- /dev/null +++ b/tests-integration/fixtures/checking/311_prim_qualified/Main.purs @@ -0,0 +1,3 @@ +module Main where + +foreign import data Test :: Array Prim.Int diff --git a/tests-integration/fixtures/checking/311_prim_qualified/Main.snap b/tests-integration/fixtures/checking/311_prim_qualified/Main.snap new file mode 100644 index 00000000..e0d9f949 --- /dev/null +++ b/tests-integration/fixtures/checking/311_prim_qualified/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Test :: Array Int + +Roles +Test = [] diff --git a/tests-integration/fixtures/checking/312_prim_qualified_override/Main.purs b/tests-integration/fixtures/checking/312_prim_qualified_override/Main.purs new file mode 100644 index 00000000..1a088328 --- /dev/null +++ b/tests-integration/fixtures/checking/312_prim_qualified_override/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Prim (String) as Prim + +foreign import data Test :: Array Prim.Int diff --git a/tests-integration/fixtures/checking/312_prim_qualified_override/Main.snap b/tests-integration/fixtures/checking/312_prim_qualified_override/Main.snap new file mode 100644 index 00000000..4e741bdf --- /dev/null +++ b/tests-integration/fixtures/checking/312_prim_qualified_override/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Test :: Array ??? + +Roles +Test = [] + +Diagnostics +error[NotInScope]: 'Prim.Int' is not in scope + --> 5:35..5:43 + | +5 | foreign import data Test :: Array Prim.Int + | ^~~~~~~~ +error[CannotUnify]: Cannot unify '???' with 'Type' + --> 5:35..5:43 + | +5 | foreign import data Test :: Array Prim.Int + | ^~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 21ce64f9..f3fea063 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -633,3 +633,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_309_synonym_function_result_kind_main() { run_test("309_synonym_function_result_kind", "Main"); } #[rustfmt::skip] #[test] fn test_310_synonym_forall_expansion_main() { run_test("310_synonym_forall_expansion", "Main"); } + +#[rustfmt::skip] #[test] fn test_311_prim_qualified_main() { run_test("311_prim_qualified", "Main"); } + +#[rustfmt::skip] #[test] fn test_312_prim_qualified_override_main() { run_test("312_prim_qualified_override", "Main"); } From adc303d36751cc4de79da81ba60423c7fd921051 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Feb 2026 04:08:23 +0800 Subject: [PATCH 135/386] Implement check_guarded_expression This implements a proper checking rule for guarded expressions to fix the erroneous implementation which overwrote types instead of unifying. --- .../checking/src/algorithm/equation.rs | 3 +- compiler-core/checking/src/algorithm/term.rs | 49 +++++++++++++++++++ .../080_let_recursive_errors/Main.snap | 12 ++--- .../221_do_let_annotation_solve/Main.snap | 6 +-- .../223_ado_let_annotation_solve/Main.snap | 6 +-- .../Main.snap | 6 +-- .../Main.snap | 6 +-- .../Main.snap | 12 ++--- .../Main.snap | 6 +-- .../checking/248_do_empty_block/Main.snap | 6 +-- .../checking/250_do_final_let/Main.snap | 6 +-- .../Main.purs | 14 ++++++ .../Main.snap | 33 +++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 14 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.purs create mode 100644 tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap diff --git a/compiler-core/checking/src/algorithm/equation.rs b/compiler-core/checking/src/algorithm/equation.rs index 1d01a29e..7a047f69 100644 --- a/compiler-core/checking/src/algorithm/equation.rs +++ b/compiler-core/checking/src/algorithm/equation.rs @@ -270,8 +270,7 @@ where }; if let Some(guarded) = &equation.guarded { - let inferred_type = term::infer_guarded_expression(state, context, guarded)?; - let _ = unification::subtype(state, context, inferred_type, expected_type)?; + term::check_guarded_expression(state, context, guarded, expected_type)?; } } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 7395ca17..df8b11e0 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1826,6 +1826,37 @@ where } } +pub fn check_guarded_expression( + state: &mut CheckState, + context: &CheckContext, + guarded: &lowering::GuardedExpression, + expected: TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match guarded { + lowering::GuardedExpression::Unconditional { where_expression } => { + let Some(w) = where_expression else { + return Ok(()); + }; + check_where_expression(state, context, w, expected)?; + Ok(()) + } + lowering::GuardedExpression::Conditionals { pattern_guarded } => { + for pattern_guarded in pattern_guarded.iter() { + for pattern_guard in pattern_guarded.pattern_guards.iter() { + check_pattern_guard(state, context, pattern_guard)?; + } + if let Some(w) = &pattern_guarded.where_expression { + check_where_expression(state, context, w, expected)?; + } + } + Ok(()) + } + } +} + fn check_pattern_guard( state: &mut CheckState, context: &CheckContext, @@ -1865,3 +1896,21 @@ where infer_expression(state, context, expression) } + +fn check_where_expression( + state: &mut CheckState, + context: &CheckContext, + where_expression: &lowering::WhereExpression, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + check_let_chunks(state, context, &where_expression.bindings)?; + + let Some(expression) = where_expression.expression else { + return Ok(context.prim.unknown); + }; + + check_expression(state, context, expression, expected) +} diff --git a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap index fc31e7a0..2283b3d4 100644 --- a/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap +++ b/tests-integration/fixtures/checking/080_let_recursive_errors/Main.snap @@ -14,15 +14,15 @@ Types Diagnostics error[CannotUnify]: Cannot unify 'Int' with 'String' - --> 10:7..11:16 + --> 11:13..11:16 | -10 | g :: Int -> String - | ^~~~~~~~~~~~~~~~~~ +11 | g x = f x + | ^~~ error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 8:7..9:16 + --> 9:13..9:16 | -8 | let f :: Int -> Int - | ^~~~~~~~~~~~~~~ +9 | f x = g x + | ^~~ error[CannotUnify]: Cannot unify '?2[:0] -> ?3[:0]' with '?3[:0]' --> 17:7..17:14 | diff --git a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap index 7b65faff..3dfcee3b 100644 --- a/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/221_do_let_annotation_solve/Main.snap @@ -23,7 +23,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 18:5..19:10 + --> 19:9..19:10 | -18 | f :: Int - | ^~~~~~~~ +19 | f = a + | ^ diff --git a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap index 11b69a15..a6f095a7 100644 --- a/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap +++ b/tests-integration/fixtures/checking/223_ado_let_annotation_solve/Main.snap @@ -25,7 +25,7 @@ Unit = [] Diagnostics error[CannotUnify]: Cannot unify 'String' with 'Int' - --> 18:5..19:10 + --> 19:9..19:10 | -18 | f :: Int - | ^~~~~~~~ +19 | f = a + | ^ diff --git a/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap index 6e33b6dc..661ca086 100644 --- a/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap +++ b/tests-integration/fixtures/checking/228_record_expression_missing_field/Main.snap @@ -10,7 +10,7 @@ Types Diagnostics error[PropertyIsMissing]: Missing required properties: b - --> 3:1..3:31 + --> 4:8..4:16 | -3 | test :: { a :: Int, b :: Int } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 | test = { a: 1 } + | ^~~~~~~~ diff --git a/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap index 7c939f10..3262091e 100644 --- a/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap +++ b/tests-integration/fixtures/checking/229_record_expression_additional_field/Main.snap @@ -10,7 +10,7 @@ Types Diagnostics error[AdditionalProperty]: Additional properties not allowed: b - --> 3:1..3:21 + --> 4:8..4:22 | -3 | test :: { a :: Int } - | ^~~~~~~~~~~~~~~~~~~~ +4 | test = { a: 1, b: 2 } + | ^~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap index 50a1ab16..b47d949d 100644 --- a/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap +++ b/tests-integration/fixtures/checking/230_record_expression_missing_and_additional/Main.snap @@ -10,12 +10,12 @@ Types Diagnostics error[PropertyIsMissing]: Missing required properties: b - --> 3:1..3:31 + --> 4:8..4:22 | -3 | test :: { a :: Int, b :: Int } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 | test = { a: 1, c: 3 } + | ^~~~~~~~~~~~~~ error[AdditionalProperty]: Additional properties not allowed: c - --> 3:1..3:31 + --> 4:8..4:22 | -3 | test :: { a :: Int, b :: Int } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 | test = { a: 1, c: 3 } + | ^~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap index 398d4f08..d214fb2b 100644 --- a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap +++ b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap @@ -10,7 +10,7 @@ Types Diagnostics error[AdditionalProperty]: Additional properties not allowed: y - --> 3:1..3:34 + --> 4:8..4:33 | -3 | test :: { outer :: { x :: Int } } - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 | test = { outer: { x: 1, y: 2 } } + | ^~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/248_do_empty_block/Main.snap b/tests-integration/fixtures/checking/248_do_empty_block/Main.snap index b9efdf78..4de116a3 100644 --- a/tests-integration/fixtures/checking/248_do_empty_block/Main.snap +++ b/tests-integration/fixtures/checking/248_do_empty_block/Main.snap @@ -16,10 +16,10 @@ error[EmptyDoBlock]: Empty do block 6 | test = do | ^~ error[CannotUnify]: Cannot unify '???' with 'Effect Int' - --> 5:1..5:19 + --> 6:8..6:10 | -5 | test :: Effect Int - | ^~~~~~~~~~~~~~~~~~ +6 | test = do + | ^~ error[EmptyDoBlock]: Empty do block --> 8:9..8:11 | diff --git a/tests-integration/fixtures/checking/250_do_final_let/Main.snap b/tests-integration/fixtures/checking/250_do_final_let/Main.snap index e3b6b93c..bb36dbd1 100644 --- a/tests-integration/fixtures/checking/250_do_final_let/Main.snap +++ b/tests-integration/fixtures/checking/250_do_final_let/Main.snap @@ -16,10 +16,10 @@ error[InvalidFinalLet]: Invalid final let statement in do expression 7 | let x = 1 | ^~~~~~~~~ error[CannotUnify]: Cannot unify '???' with 'Effect Int' - --> 5:1..5:19 + --> 6:8..7:12 | -5 | test :: Effect Int - | ^~~~~~~~~~~~~~~~~~ +6 | test = do + | ^~ error[InvalidFinalLet]: Invalid final let statement in do expression --> 10:3..10:12 | diff --git a/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.purs b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.purs new file mode 100644 index 00000000..fbcb6fb6 --- /dev/null +++ b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.purs @@ -0,0 +1,14 @@ +module Main where + +class Generate f where + generate :: forall a b. (b -> a) -> b -> f a + +data List a = Nil | Cons a + +instance Generate List where + generate f b = Cons (f b) + +test :: Int -> List Int +test start + | true = generate (\x -> x) start + | true = generate (\x -> x) start diff --git a/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap new file mode 100644 index 00000000..1a23bd24 --- /dev/null +++ b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap @@ -0,0 +1,33 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +generate :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Generate (f :: Type -> Type) => + ((b :: Type) -> (a :: Type)) -> (b :: Type) -> (f :: Type -> Type) (a :: Type) +Nil :: forall (a :: Type). List (a :: Type) +Cons :: forall (a :: Type). (a :: Type) -> List (a :: Type) +test :: Int -> List Int + +Types +Generate :: (Type -> Type) -> Constraint +List :: Type -> Type + +Data +List + Quantified = :0 + Kind = :0 + + +Roles +List = [Representational] + +Classes +class Generate (&0 :: Type -> Type) + +Instances +instance Generate (List :: Type -> Type) + chain: 0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index f3fea063..1acbf422 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -637,3 +637,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_311_prim_qualified_main() { run_test("311_prim_qualified", "Main"); } #[rustfmt::skip] #[test] fn test_312_prim_qualified_override_main() { run_test("312_prim_qualified_override", "Main"); } + +#[rustfmt::skip] #[test] fn test_313_guarded_constraint_propagation_main() { run_test("313_guarded_constraint_propagation", "Main"); } From ee27ea99853659ced7b0df3abd8a65f4534ebabf Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Feb 2026 04:20:08 +0800 Subject: [PATCH 136/386] Fix skolem peeling for the Type::Function form --- .../checking/src/algorithm/derive.rs | 20 +++++++++++++--- .../314_derive_newtype_function/Main.purs | 9 +++++++ .../314_derive_newtype_function/Main.snap | 24 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tests-integration/fixtures/checking/314_derive_newtype_function/Main.purs create mode 100644 tests-integration/fixtures/checking/314_derive_newtype_function/Main.snap diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index b213b1cc..c7b677d6 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -265,7 +265,9 @@ where let inner_type = if skolem_count == 0 { inner_type - } else if let Some(inner_type) = try_peel_trailing_skolems(state, inner_type, skolem_count) { + } else if let Some(inner_type) = + try_peel_trailing_skolems(state, context, inner_type, skolem_count) + { inner_type } else { state.insert_error(ErrorKind::InvalidNewtypeDeriveSkolemArguments); @@ -399,11 +401,15 @@ fn generate_delegate_constraint( state.push_wanted(wanted_constraint); } -fn try_peel_trailing_skolems( +fn try_peel_trailing_skolems( state: &mut CheckState, + context: &CheckContext, mut type_id: TypeId, mut count: usize, -) -> Option { +) -> Option +where + Q: ExternalQueries, +{ safe_loop! { if count == 0 { break Some(type_id); @@ -419,6 +425,14 @@ fn try_peel_trailing_skolems( } else { break None; } + } else if let Type::Function(argument, result) = state.storage[type_id] { + let result = state.normalize_type(result); + if matches!(state.storage[result], Type::Variable(Variable::Skolem(_, _))) { + count -= 1; + type_id = state.storage.intern(Type::Application(context.prim.function, argument)); + } else { + break None; + } } else { break None; } diff --git a/tests-integration/fixtures/checking/314_derive_newtype_function/Main.purs b/tests-integration/fixtures/checking/314_derive_newtype_function/Main.purs new file mode 100644 index 00000000..4fe996dd --- /dev/null +++ b/tests-integration/fixtures/checking/314_derive_newtype_function/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Semigroupoid (class Semigroupoid) +import Control.Category (class Category) + +newtype Builder a b = Builder (a -> b) + +derive newtype instance Semigroupoid Builder +derive newtype instance Category Builder diff --git a/tests-integration/fixtures/checking/314_derive_newtype_function/Main.snap b/tests-integration/fixtures/checking/314_derive_newtype_function/Main.snap new file mode 100644 index 00000000..47fb4186 --- /dev/null +++ b/tests-integration/fixtures/checking/314_derive_newtype_function/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Builder :: + forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> Builder (a :: Type) (b :: Type) + +Types +Builder :: Type -> Type -> Type + +Data +Builder + Quantified = :0 + Kind = :0 + + +Roles +Builder = [Representational, Representational] + +Derived +derive Semigroupoid (Builder :: Type -> Type -> Type) +derive Category (Builder :: Type -> Type -> Type) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 1acbf422..2dbffef4 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -639,3 +639,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_312_prim_qualified_override_main() { run_test("312_prim_qualified_override", "Main"); } #[rustfmt::skip] #[test] fn test_313_guarded_constraint_propagation_main() { run_test("313_guarded_constraint_propagation", "Main"); } + +#[rustfmt::skip] #[test] fn test_314_derive_newtype_function_main() { run_test("314_derive_newtype_function", "Main"); } From 507b521751b2debf72da8e48dddf9aa1af6927a5 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Feb 2026 04:46:17 +0800 Subject: [PATCH 137/386] Fix binding power collision for adjacent precedence levels The Pratt parser's binding_power function used p+1/p+2 spacing, which gave each precedence level only a single-slot gap. This caused adjacent levels with different associativities to collide: infixr 3 produced lbp=5, while infix 4 produced rbp=5, so the parser could not distinguish them. Scaling by 2*p gives each level a two-slot range, preventing overlap. Co-Authored-By: Claude Opus 4.6 --- compiler-core/sugar/src/bracketing.rs | 113 +++++++++++++++++- .../315_operator_chain_mixed_fixity/Main.purs | 24 ++++ .../315_operator_chain_mixed_fixity/Main.snap | 15 +++ tests-integration/tests/checking/generated.rs | 2 + 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.purs create mode 100644 tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.snap diff --git a/compiler-core/sugar/src/bracketing.rs b/compiler-core/sugar/src/bracketing.rs index 94d1b8f5..d9c75aef 100644 --- a/compiler-core/sugar/src/bracketing.rs +++ b/compiler-core/sugar/src/bracketing.rs @@ -91,9 +91,16 @@ where } /// Translates [`Associativity`] and precedence into binding power. +/// +/// Each precedence level occupies two binding power slots so that +/// adjacent precedence levels never overlap. Without the `2*p` scaling, +/// `infixr 3` and `infix 4` both produce a binding power of 5, which +/// prevents the Pratt parser from breaking out of a higher-precedence +/// recursive call when it encounters a lower-precedence operator. fn binding_power(associativity: Associativity, precedence: u8) -> (u8, u8) { - let bp_0 = precedence.saturating_add(1); - let bp_1 = precedence.saturating_add(2); + let base = precedence.saturating_mul(2); + let bp_0 = base.saturating_add(1); + let bp_1 = base.saturating_add(2); match associativity { Associativity::None => (bp_0, bp_0), Associativity::Left => (bp_0, bp_1), @@ -257,3 +264,105 @@ pub fn bracketed( Ok(Bracketed { binders, expressions, types }) } + +#[cfg(test)] +mod tests { + use super::*; + + const ALL_ASSOCIATIVITIES: [Associativity; 3] = + [Associativity::None, Associativity::Left, Associativity::Right]; + + /// PureScript precedences range from 0 to 9. + const PRECEDENCE_RANGE: std::ops::RangeInclusive = 0..=9; + + /// A lower-precedence operator must always yield to a recursive call + /// from a higher-precedence operator. Without sufficient spacing in + /// the binding power encoding, adjacent precedences can collide. + /// + /// Let `bp(a, p) = (l, r)` denote the binding power function. Then: + /// + /// ```text + /// forall p1, p2 in 0..=9, a1, a2 in {None, Left, Right}. + /// p1 < p2 => fst(bp(a1, p1)) < snd(bp(a2, p2)) + /// ``` + #[test] + fn lower_precedence_yields() { + for p_high in PRECEDENCE_RANGE { + for p_low in 0..p_high { + for &assoc_high in &ALL_ASSOCIATIVITIES { + let (_, right_bp_high) = binding_power(assoc_high, p_high); + for &assoc_low in &ALL_ASSOCIATIVITIES { + let (left_bp_low, _) = binding_power(assoc_low, p_low); + assert!( + left_bp_low < right_bp_high, + "precedence {p_low} ({assoc_low:?}) should yield to \ + precedence {p_high} ({assoc_high:?}): \ + lbp {left_bp_low} must be < rbp {right_bp_high}" + ); + } + } + } + } + } + + /// A higher-precedence operator must bind tighter than a recursive + /// call from a lower-precedence operator. + /// + /// ```text + /// forall p1, p2 in 0..=9, a1, a2 in {None, Left, Right}. + /// p1 < p2 => fst(bp(a2, p2)) >= snd(bp(a1, p1)) + /// ``` + #[test] + fn higher_precedence_binds() { + for p_low in PRECEDENCE_RANGE { + for p_high in (p_low + 1)..=9 { + for &assoc_low in &ALL_ASSOCIATIVITIES { + let (_, right_bp_low) = binding_power(assoc_low, p_low); + for &assoc_high in &ALL_ASSOCIATIVITIES { + let (left_bp_high, _) = binding_power(assoc_high, p_high); + assert!( + left_bp_high >= right_bp_low, + "precedence {p_high} ({assoc_high:?}) should bind inside \ + precedence {p_low} ({assoc_low:?}): \ + lbp {left_bp_high} must be >= rbp {right_bp_low}" + ); + } + } + } + } + } + + /// Left-associative operators at the same precedence continue the + /// current branch rather than recursing deeper. + /// + /// ```text + /// forall p in 0..=9. fst(bp(Left, p)) < snd(bp(Left, p)) + /// ``` + #[test] + fn left_associative_chains_left() { + for p in PRECEDENCE_RANGE { + let (left_bp, right_bp) = binding_power(Associativity::Left, p); + assert!( + left_bp < right_bp, + "left-associative at precedence {p}: lbp {left_bp} must be < rbp {right_bp}" + ); + } + } + + /// Right-associative operators at the same precedence recurse deeper + /// into the right subtree. + /// + /// ```text + /// forall p in 0..=9. fst(bp(Right, p)) > snd(bp(Right, p)) + /// ``` + #[test] + fn right_associative_chains_right() { + for p in PRECEDENCE_RANGE { + let (left_bp, right_bp) = binding_power(Associativity::Right, p); + assert!( + left_bp > right_bp, + "right-associative at precedence {p}: lbp {left_bp} must be > rbp {right_bp}" + ); + } + } +} diff --git a/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.purs b/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.purs new file mode 100644 index 00000000..67024115 --- /dev/null +++ b/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Data.Eq (class Eq) + +eq :: forall a. Eq a => a -> a -> Boolean +eq _ _ = true + +conj :: Boolean -> Boolean -> Boolean +conj _ _ = true + +infix 4 eq as == +infixr 3 conj as && + +-- Single operator: should work +test1 :: Int -> Boolean +test1 h = h == 2 + +-- Single &&: should work +test2 :: Boolean +test2 = true && true + +-- Mixed chain: the failing case +test3 :: Int -> Int -> Boolean +test3 h rh = h == 2 && rh == 1 diff --git a/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.snap b/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.snap new file mode 100644 index 00000000..a6b30361 --- /dev/null +++ b/tests-integration/fixtures/checking/315_operator_chain_mixed_fixity/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +conj :: Boolean -> Boolean -> Boolean +== :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +&& :: Boolean -> Boolean -> Boolean +test1 :: Int -> Boolean +test2 :: Boolean +test3 :: Int -> Int -> Boolean + +Types diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 2dbffef4..a7ce581f 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -641,3 +641,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_313_guarded_constraint_propagation_main() { run_test("313_guarded_constraint_propagation", "Main"); } #[rustfmt::skip] #[test] fn test_314_derive_newtype_function_main() { run_test("314_derive_newtype_function", "Main"); } + +#[rustfmt::skip] #[test] fn test_315_operator_chain_mixed_fixity_main() { run_test("315_operator_chain_mixed_fixity", "Main"); } From 898ace3db1ad59a7dbf0e3943f259db289253fd3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 9 Feb 2026 01:32:19 +0800 Subject: [PATCH 138/386] Move normalise_expand_type to dedicated module --- compiler-core/checking/src/algorithm.rs | 3 + .../src/algorithm/exhaustiveness/convert.rs | 5 +- .../checking/src/algorithm/inspect.rs | 7 +- .../checking/src/algorithm/kind/operator.rs | 46 +++++++++++ .../checking/src/algorithm/kind/synonym.rs | 77 +------------------ .../checking/src/algorithm/normalise.rs | 28 +++++++ compiler-core/checking/src/algorithm/term.rs | 6 +- .../checking/src/algorithm/term_item.rs | 6 +- .../checking/src/algorithm/unification.rs | 15 ++-- 9 files changed, 98 insertions(+), 95 deletions(-) create mode 100644 compiler-core/checking/src/algorithm/normalise.rs diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 03b58489..29844940 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -29,6 +29,9 @@ pub mod inspect; /// Implements kind inference and checking for [`lowering::TypeKind`]. pub mod kind; +/// Implements type normalisation for unification variables, operators, synonyms. +pub mod normalise; + /// Implements surface-generic operator chain inference. pub mod operator; diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index 9017fc93..3b77ca18 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -11,9 +11,8 @@ use sugar::OperatorTree; use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId}; -use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; -use crate::algorithm::toolkit; +use crate::algorithm::{normalise, toolkit}; use crate::core::{Type, TypeId}; pub fn convert_binder( @@ -137,7 +136,7 @@ fn try_build_record_constructor( where Q: ExternalQueries, { - let expanded_t = synonym::normalize_expand_type(state, context, t)?; + let expanded_t = normalise::normalise_expand_type(state, context, t)?; let (constructor, arguments) = toolkit::extract_type_application(state, expanded_t); diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index 51157979..fb870a1d 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -4,10 +4,9 @@ use building_types::QueryResult; use lowering::TypeVariableBindingId; use crate::ExternalQueries; -use crate::algorithm::kind::synonym; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::substitute; +use crate::algorithm::{normalise, substitute}; use crate::core::{ForallBinder, Type, TypeId, Variable, debruijn}; pub struct InspectSignature { @@ -86,7 +85,7 @@ where let mut bindings = substitute::LevelToType::default(); safe_loop! { - current_id = synonym::normalize_expand_type(state, context, current_id)?; + current_id = normalise::normalise_expand_type(state, context, current_id)?; // In the example, after the Forall case has peeled f:0, g:1, and the // synonym-expanded a:2, the accumulated bindings are the following: @@ -157,7 +156,7 @@ where let mut bindings = substitute::LevelToType::default(); safe_loop! { - current_id = synonym::normalize_expand_type(state, context, current_id)?; + current_id = normalise::normalise_expand_type(state, context, current_id)?; if !matches!(state.storage[current_id], Type::Forall(..)) && !bindings.is_empty() { current_id = substitute::SubstituteBindings::on(state, &bindings, current_id); diff --git a/compiler-core/checking/src/algorithm/kind/operator.rs b/compiler-core/checking/src/algorithm/kind/operator.rs index 4b2d2e58..d5b382fa 100644 --- a/compiler-core/checking/src/algorithm/kind/operator.rs +++ b/compiler-core/checking/src/algorithm/kind/operator.rs @@ -3,6 +3,7 @@ use building_types::QueryResult; use files::FileId; use indexing::TypeItemId; +use lowering::{LoweredModule, TypeItemIr}; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; @@ -44,3 +45,48 @@ where Ok(result_kind) } + +pub fn resolve_type_operator_target( + lowered: &LoweredModule, + item_id: TypeItemId, +) -> Option<(FileId, TypeItemId)> { + let TypeItemIr::Operator { resolution, .. } = lowered.info.get_type_item(item_id)? else { + return None; + }; + *resolution +} + +pub fn expand_type_operator( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Type::OperatorApplication(file_id, item_id, left, right) = state.storage[type_id] else { + return Ok(type_id); + }; + + let resolution = if file_id == context.id { + context.lowered.info.get_type_item(item_id).and_then(|ir| match ir { + TypeItemIr::Operator { resolution, .. } => *resolution, + _ => None, + }) + } else { + context.queries.lowered(file_id)?.info.get_type_item(item_id).and_then(|ir| match ir { + TypeItemIr::Operator { resolution, .. } => *resolution, + _ => None, + }) + }; + + let Some((file_id, item_id)) = resolution else { + return Ok(type_id); + }; + + let constructor = state.storage.intern(Type::Constructor(file_id, item_id)); + let left = state.storage.intern(Type::Application(constructor, left)); + let right = state.storage.intern(Type::Application(left, right)); + + Ok(right) +} diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index 2fd0cd25..cac0962f 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -12,7 +12,7 @@ use building_types::QueryResult; use files::FileId; use indexing::TypeItemId; use itertools::Itertools; -use lowering::{GroupedModule, LoweredModule, TypeItemIr}; +use lowering::GroupedModule; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, substitute, transfer, unification}; @@ -363,16 +363,6 @@ enum DiscoveredSynonym { Additional { synonym: Synonym, arguments: Arc<[TypeId]>, additional: Vec }, } -fn resolve_operator_target( - lowered: &LoweredModule, - item_id: TypeItemId, -) -> Option<(FileId, TypeItemId)> { - let TypeItemIr::Operator { resolution, .. } = lowered.info.get_type_item(item_id)? else { - return None; - }; - *resolution -} - fn discover_synonym_application( state: &mut CheckState, context: &CheckContext, @@ -463,10 +453,10 @@ where Type::OperatorApplication(operator_file_id, operator_item_id, left, right) => { let resolution = if operator_file_id == context.id { - resolve_operator_target(&context.lowered, operator_item_id) + kind::operator::resolve_type_operator_target(&context.lowered, operator_item_id) } else { let lowered = context.queries.lowered(operator_file_id)?; - resolve_operator_target(&lowered, operator_item_id) + kind::operator::resolve_type_operator_target(&lowered, operator_item_id) }; let Some((file_id, item_id)) = resolution else { @@ -578,64 +568,3 @@ where } }) } - -/// Normalises a type operator application to constructor application form. -fn expand_type_operator( - state: &mut CheckState, - context: &CheckContext, - type_id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let Type::OperatorApplication(file_id, item_id, left, right) = state.storage[type_id] else { - return Ok(type_id); - }; - - let resolution = if file_id == context.id { - context.lowered.info.get_type_item(item_id).and_then(|ir| match ir { - TypeItemIr::Operator { resolution, .. } => *resolution, - _ => None, - }) - } else { - context.queries.lowered(file_id)?.info.get_type_item(item_id).and_then(|ir| match ir { - TypeItemIr::Operator { resolution, .. } => *resolution, - _ => None, - }) - }; - - let Some((file_id, item_id)) = resolution else { - return Ok(type_id); - }; - - let constructor = state.storage.intern(Type::Constructor(file_id, item_id)); - let left = state.storage.intern(Type::Application(constructor, left)); - let right = state.storage.intern(Type::Application(left, right)); - - Ok(right) -} - -pub fn normalize_expand_type( - state: &mut CheckState, - context: &CheckContext, - mut type_id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - const EXPANSION_LIMIT: u32 = 1_000_000; - - for _ in 0..EXPANSION_LIMIT { - let normalized_id = state.normalize_type(type_id); - let expanded_id = expand_type_operator(state, context, normalized_id)?; - let expanded_id = expand_type_synonym(state, context, expanded_id)?; - - if expanded_id == type_id { - return Ok(type_id); - } - - type_id = expanded_id; - } - - unreachable!("critical violation: limit reached in normalize_expand_type") -} diff --git a/compiler-core/checking/src/algorithm/normalise.rs b/compiler-core/checking/src/algorithm/normalise.rs new file mode 100644 index 00000000..175a2c2c --- /dev/null +++ b/compiler-core/checking/src/algorithm/normalise.rs @@ -0,0 +1,28 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::algorithm::kind::{operator, synonym}; +use crate::algorithm::safety::safe_loop; +use crate::algorithm::state::{CheckContext, CheckState}; +use crate::core::TypeId; + +pub fn normalise_expand_type( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + let expanded_id = state.normalize_type(type_id); + let expanded_id = operator::expand_type_operator(state, context, expanded_id)?; + let expanded_id = synonym::expand_type_synonym(state, context, expanded_id)?; + + if expanded_id == type_id { + return Ok(type_id); + } + + type_id = expanded_id; + } +} diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index df8b11e0..8006406b 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -8,8 +8,8 @@ use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{ - binder, equation, exhaustiveness, inspect, kind, operator, substitute, toolkit, transfer, - unification, + binder, equation, exhaustiveness, inspect, kind, normalise, operator, substitute, toolkit, + transfer, unification, }; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::{ErrorKind, ErrorStep}; @@ -1557,7 +1557,7 @@ where F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { crate::trace_fields!(state, context, { function = function_t }); - let function_t = kind::synonym::normalize_expand_type(state, context, function_t)?; + let function_t = normalise::normalise_expand_type(state, context, function_t)?; match state.storage[function_t] { // Check that `argument_id :: argument_type` Type::Function(argument_type, result_type) => { diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index fce6d387..a9fb8020 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -7,11 +7,11 @@ use itertools::Itertools; use lowering::TermItemIr; use crate::ExternalQueries; -use crate::algorithm::kind::synonym; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; use crate::algorithm::{ - constraint, equation, inspect, kind, quantify, substitute, term, transfer, unification, + constraint, equation, inspect, kind, normalise, quantify, substitute, term, transfer, + unification, }; use crate::core::{Instance, InstanceKind, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -231,7 +231,7 @@ where let mut current = class_kind; for _ in 0..FUEL { - current = synonym::normalize_expand_type(state, context, current)?; + current = normalise::normalise_expand_type(state, context, current)?; match state.storage[current] { Type::Forall(ref binder, inner) => { let binder_level = binder.level; diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index e5982e92..9901c141 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -4,9 +4,8 @@ use building_types::QueryResult; use itertools::{EitherOrBoth, Itertools}; use crate::ExternalQueries; -use crate::algorithm::kind::synonym; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{kind, substitute}; +use crate::algorithm::{kind, normalise, substitute}; use crate::core::{RowField, RowType, Type, TypeId, Variable, debruijn}; use crate::error::ErrorKind; @@ -89,8 +88,8 @@ pub fn subtype_with_mode( where Q: ExternalQueries, { - let t1 = synonym::normalize_expand_type(state, context, t1)?; - let t2 = synonym::normalize_expand_type(state, context, t2)?; + let t1 = normalise::normalise_expand_type(state, context, t1)?; + let t2 = normalise::normalise_expand_type(state, context, t2)?; crate::debug_fields!(state, context, { t1 = t1, t2 = t2, ?mode = mode }); @@ -145,8 +144,8 @@ where Type::Application(t1_function, t1_argument), Type::Application(t2_function, t2_argument), ) if t1_function == context.prim.record && t2_function == context.prim.record => { - let t1_argument = synonym::normalize_expand_type(state, context, t1_argument)?; - let t2_argument = synonym::normalize_expand_type(state, context, t2_argument)?; + let t1_argument = normalise::normalise_expand_type(state, context, t1_argument)?; + let t2_argument = normalise::normalise_expand_type(state, context, t2_argument)?; let t1_core = state.storage[t1_argument].clone(); let t2_core = state.storage[t2_argument].clone(); @@ -172,8 +171,8 @@ pub fn unify( where Q: ExternalQueries, { - let t1 = synonym::normalize_expand_type(state, context, t1)?; - let t2 = synonym::normalize_expand_type(state, context, t2)?; + let t1 = normalise::normalise_expand_type(state, context, t1)?; + let t2 = normalise::normalise_expand_type(state, context, t2)?; crate::debug_fields!(state, context, { t1 = t1, t2 = t2 }); From c387b4a80cd12ea9c5c3555ca764e258d5e5baa9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 9 Feb 2026 01:51:30 +0800 Subject: [PATCH 139/386] Fix synonyms in delegate constraints for 'derive newtype' --- .../checking/src/algorithm/derive.rs | 5 +++- .../checking/316_synonym_derive/Main.purs | 12 ++++++++ .../checking/316_synonym_derive/Main.snap | 28 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests-integration/fixtures/checking/316_synonym_derive/Main.purs create mode 100644 tests-integration/fixtures/checking/316_synonym_derive/Main.snap diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index c7b677d6..fd143da5 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -20,7 +20,7 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::VarianceConfig; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{kind, term_item, toolkit, transfer}; +use crate::algorithm::{kind, normalise, term_item, toolkit, transfer}; use crate::core::{Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -274,6 +274,9 @@ where return Ok(None); }; + // Make sure that the constraint solver sees the synonym expansion. + let inner_type = normalise::normalise_expand_type(state, context, inner_type)?; + let delegate_constraint = { let class_type = state.storage.intern(Type::Constructor(input.class_file, input.class_id)); diff --git a/tests-integration/fixtures/checking/316_synonym_derive/Main.purs b/tests-integration/fixtures/checking/316_synonym_derive/Main.purs new file mode 100644 index 00000000..5fc20cd0 --- /dev/null +++ b/tests-integration/fixtures/checking/316_synonym_derive/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Eq (class Eq) + +foreign import data State :: Type + +instance Eq State where + eq _ _ = true + +newtype Test = Test State + +derive newtype instance Eq Test diff --git a/tests-integration/fixtures/checking/316_synonym_derive/Main.snap b/tests-integration/fixtures/checking/316_synonym_derive/Main.snap new file mode 100644 index 00000000..f698b291 --- /dev/null +++ b/tests-integration/fixtures/checking/316_synonym_derive/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Test :: State -> Test + +Types +State :: Type +Test :: Type + +Data +Test + Quantified = :0 + Kind = :0 + + +Roles +State = [] +Test = [] + +Instances +instance Eq (State :: Type) + chain: 0 + +Derived +derive Eq (Test :: Type) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index a7ce581f..cea0926f 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -643,3 +643,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_314_derive_newtype_function_main() { run_test("314_derive_newtype_function", "Main"); } #[rustfmt::skip] #[test] fn test_315_operator_chain_mixed_fixity_main() { run_test("315_operator_chain_mixed_fixity", "Main"); } + +#[rustfmt::skip] #[test] fn test_316_synonym_derive_main() { run_test("316_synonym_derive", "Main"); } From bb25a707cc91a4471b8397d51a4735e2a50c8f5d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 10 Feb 2026 04:02:16 +0800 Subject: [PATCH 140/386] Fix higher rank fields in record subtyping --- .../checking/src/algorithm/binder.rs | 240 ++++++++++++++---- .../checking/src/algorithm/unification.rs | 11 +- .../checking/061_record_binder/Main.snap | 18 +- .../Main.snap | 4 +- .../checking/317_higher_rank_fields/Main.purs | 9 + .../checking/317_higher_rank_fields/Main.snap | 17 ++ tests-integration/tests/checking/generated.rs | 2 + 7 files changed, 237 insertions(+), 64 deletions(-) create mode 100644 tests-integration/fixtures/checking/317_higher_rank_fields/Main.purs create mode 100644 tests-integration/fixtures/checking/317_higher_rank_fields/Main.snap diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index bc1bc629..92625b3a 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -1,12 +1,15 @@ +use std::sync::Arc; + use building_types::QueryResult; +use itertools::{EitherOrBoth, Itertools}; use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification::ElaborationMode; -use crate::algorithm::{binder, kind, operator, term, toolkit, unification}; +use crate::algorithm::{binder, kind, normalise, operator, term, toolkit, unification}; use crate::core::{RowField, RowType, Type, TypeId}; -use crate::error::ErrorStep; +use crate::error::{ErrorKind, ErrorStep}; #[derive(Copy, Clone, Debug)] enum BinderMode { @@ -258,71 +261,216 @@ where } lowering::BinderKind::Record { record } => { - let mut fields = vec![]; + if let BinderMode::Check { expected_type, elaboration } = mode { + check_record_binder(state, context, binder_id, record, expected_type, elaboration) + } else { + infer_record_binder(state, context, binder_id, record) + } + } - for field in record.iter() { - match field { - lowering::BinderRecordItem::RecordField { name, value } => { - let Some(name) = name else { continue }; - let Some(value) = value else { continue }; + lowering::BinderKind::Parenthesized { parenthesized } => { + let Some(parenthesized) = parenthesized else { return Ok(unknown) }; + binder_core(state, context, *parenthesized, mode) + } + } +} - let label = SmolStr::clone(name); - let id = infer_binder(state, context, *value)?; - fields.push(RowField { label, id }); - } - lowering::BinderRecordItem::RecordPun { id, name } => { - let Some(name) = name else { continue }; +fn check_constructor_binder_application( + state: &mut CheckState, + context: &CheckContext, + constructor_t: TypeId, + binder_id: lowering::BinderId, +) -> QueryResult +where + Q: ExternalQueries, +{ + term::check_function_application_core(state, context, constructor_t, binder_id, check_binder) +} - let label = SmolStr::clone(name); - let field_type = state.fresh_unification_type(context); +enum PatternItem { + Field(lowering::BinderId), + Pun(lowering::RecordPunId), +} - state.term_scope.bind_pun(*id, field_type); - fields.push(RowField { label, id: field_type }); - } - } +fn collect_pattern_items(record: &[lowering::BinderRecordItem]) -> Vec<(SmolStr, PatternItem)> { + let mut items = vec![]; + for field in record { + match field { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + let name = SmolStr::clone(name); + items.push((name, PatternItem::Field(*value))); } + lowering::BinderRecordItem::RecordPun { id, name } => { + let Some(name) = name else { continue }; + let name = SmolStr::clone(name); + items.push((name, PatternItem::Pun(*id))); + } + } + } + items.sort_by(|a, b| a.0.cmp(&b.0)); + items +} - let row_tail = state.fresh_unification_kinded(context.prim.row_type); +fn check_pattern_item( + state: &mut CheckState, + context: &CheckContext, + item: &PatternItem, + expected_type: TypeId, + elaboration: ElaborationMode, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match *item { + PatternItem::Field(binder_id) => match elaboration { + ElaborationMode::Yes => { + check_binder(state, context, binder_id, expected_type)?; + } + ElaborationMode::No => { + check_argument_binder(state, context, binder_id, expected_type)?; + } + }, + PatternItem::Pun(pun_id) => { + state.term_scope.bind_pun(pun_id, expected_type); + } + } + Ok(()) +} - let row_type = RowType::from_unsorted(fields, Some(row_tail)); - let row_type = state.storage.intern(Type::Row(row_type)); +fn infer_record_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + record: &[lowering::BinderRecordItem], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut fields = vec![]; - let record_type = - state.storage.intern(Type::Application(context.prim.record, row_type)); + for field in record.iter() { + match field { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; - let record_type = if let BinderMode::Check { expected_type, elaboration } = mode { - unification::subtype_with_mode( - state, - context, - record_type, - expected_type, - elaboration, - )?; - expected_type - } else { - record_type - }; + let label = SmolStr::clone(name); + let id = infer_binder(state, context, *value)?; + fields.push(RowField { label, id }); + } + lowering::BinderRecordItem::RecordPun { id, name } => { + let Some(name) = name else { continue }; - state.term_scope.bind_binder(binder_id, record_type); + let label = SmolStr::clone(name); + let field_type = state.fresh_unification_type(context); - Ok(record_type) + state.term_scope.bind_pun(*id, field_type); + fields.push(RowField { label, id: field_type }); + } } + } - lowering::BinderKind::Parenthesized { parenthesized } => { - let Some(parenthesized) = parenthesized else { return Ok(unknown) }; - binder_core(state, context, *parenthesized, mode) - } + let row_tail = state.fresh_unification_kinded(context.prim.row_type); + let row_type = RowType::from_unsorted(fields, Some(row_tail)); + let row_type = state.storage.intern(Type::Row(row_type)); + let record_type = state.storage.intern(Type::Application(context.prim.record, row_type)); + + state.term_scope.bind_binder(binder_id, record_type); + Ok(record_type) +} + +fn extract_expected_row( + state: &mut CheckState, + context: &CheckContext, + expected_type: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let expected_type = normalise::normalise_expand_type(state, context, expected_type)?; + let &Type::Application(function, argument) = &state.storage[expected_type] else { + return Ok(None); + }; + if function != context.prim.record { + return Ok(None); } + let row = normalise::normalise_expand_type(state, context, argument)?; + let Type::Row(row) = &state.storage[row] else { + return Ok(None); + }; + Ok(Some(RowType::clone(row))) } -pub fn check_constructor_binder_application( +fn check_record_binder( state: &mut CheckState, context: &CheckContext, - constructor_t: TypeId, binder_id: lowering::BinderId, + record: &[lowering::BinderRecordItem], + expected_type: TypeId, + elaboration: ElaborationMode, ) -> QueryResult where Q: ExternalQueries, { - term::check_function_application_core(state, context, constructor_t, binder_id, check_binder) + let pattern_items = collect_pattern_items(record); + + let expected_type = normalise::normalise_expand_type(state, context, expected_type)?; + + let expected_row = if let &Type::Application(function, _) = &state.storage[expected_type] + && function == context.prim.record + { + extract_expected_row(state, context, expected_type)? + } else { + None + }; + + let Some(expected_row) = expected_row else { + let result = infer_record_binder(state, context, binder_id, record)?; + unification::unify(state, context, result, expected_type)?; + return Ok(expected_type); + }; + + let mut extra_fields = vec![]; + + let patterns = pattern_items.iter(); + let expected = expected_row.fields.iter(); + + for pair in patterns.merge_join_by(expected, |pattern, expected| pattern.0.cmp(&expected.label)) + { + match pair { + // If a label exists in both, perform checking + EitherOrBoth::Both((_, item), expected) => { + check_pattern_item(state, context, item, expected.id, elaboration)?; + } + // If a label only exists in the pattern, track it + EitherOrBoth::Left((label, item)) => { + let id = state.fresh_unification_type(context); + check_pattern_item(state, context, item, id, elaboration)?; + + let label = SmolStr::clone(label); + extra_fields.push(RowField { label, id }); + } + // If a label only exists in the type, do nothing + EitherOrBoth::Right(_) => (), + } + } + + if !extra_fields.is_empty() { + if let Some(tail) = expected_row.tail { + let row_tail = state.fresh_unification_kinded(context.prim.row_type); + + let row_type = RowType::from_unsorted(extra_fields, Some(row_tail)); + let row_type = state.storage.intern(Type::Row(row_type)); + + unification::unify(state, context, tail, row_type)?; + } else { + let labels = extra_fields.into_iter().map(|field| field.label); + state.insert_error(ErrorKind::AdditionalProperty { labels: Arc::from_iter(labels) }); + } + } + + state.term_scope.bind_binder(binder_id, expected_type); + Ok(expected_type) } diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 9901c141..2195cf32 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -151,7 +151,7 @@ where let t2_core = state.storage[t2_argument].clone(); if let (Type::Row(t1_row), Type::Row(t2_row)) = (t1_core, t2_core) { - subtype_record_rows(state, context, &t1_row, &t2_row, mode) + subtype_rows(state, context, &t1_row, &t2_row, mode) } else { unify(state, context, t1, t2) } @@ -479,14 +479,14 @@ pub fn promote_type( aux(state, &c, solve_depth, solution) } -/// Checks that `t1_row` is a subtype of `t2_row`, generated errors for +/// Checks that `t1_row` is a subtype of `t2_row`, generating errors for /// additional or missing fields. This is used for record subtyping. /// /// * This algorithm partitions row fields into common, t1-only, and t2-only fields. /// * If t1_row is closed and t2_row is non-empty, [`ErrorKind::PropertyIsMissing`] /// * If t2_row is closed and t1_row is non-empty, [`ErrorKind::AdditionalProperty`] #[tracing::instrument(skip_all, name = "subtype_rows")] -fn subtype_record_rows( +fn subtype_rows( state: &mut CheckState, context: &CheckContext, t1_row: &RowType, @@ -501,10 +501,7 @@ where context, t1_row, t2_row, - |state, context, left, right| { - Ok(subtype_with_mode(state, context, left, right, mode)? - && subtype_with_mode(state, context, right, left, mode)?) - }, + |state, context, left, right| subtype_with_mode(state, context, left, right, mode), )?; if !ok { diff --git a/tests-integration/fixtures/checking/061_record_binder/Main.snap b/tests-integration/fixtures/checking/061_record_binder/Main.snap index 1c4d6738..d7ceac8a 100644 --- a/tests-integration/fixtures/checking/061_record_binder/Main.snap +++ b/tests-integration/fixtures/checking/061_record_binder/Main.snap @@ -6,20 +6,20 @@ expression: report Terms test1 :: { x :: Int, y :: Int } -> Int test1' :: - forall (t12 :: Type) (t13 :: Row Type) (t14 :: Type). - { x :: (t14 :: Type), y :: (t12 :: Type) | (t13 :: Row Type) } -> (t14 :: Type) + forall (t8 :: Type) (t9 :: Row Type) (t10 :: Type). + { x :: (t10 :: Type), y :: (t8 :: Type) | (t9 :: Row Type) } -> (t10 :: Type) test2 :: { x :: Int, y :: String } -> { x :: Int, y :: String } test2' :: - forall (t20 :: Type) (t21 :: Type) (t22 :: Row Type). - { x :: (t20 :: Type), y :: (t21 :: Type) | (t22 :: Row Type) } -> - { x :: (t20 :: Type), y :: (t21 :: Type) } + forall (t12 :: Type) (t13 :: Type) (t14 :: Row Type). + { x :: (t12 :: Type), y :: (t13 :: Type) | (t14 :: Row Type) } -> + { x :: (t12 :: Type), y :: (t13 :: Type) } test3 :: { age :: Int, name :: String } -> String test3' :: - forall (t30 :: Type) (t31 :: Row Type) (t32 :: Type). - { age :: (t30 :: Type), name :: (t32 :: Type) | (t31 :: Row Type) } -> (t32 :: Type) + forall (t18 :: Type) (t19 :: Row Type) (t20 :: Type). + { age :: (t18 :: Type), name :: (t20 :: Type) | (t19 :: Row Type) } -> (t20 :: Type) nested :: { inner :: { x :: Int } } -> Int nested' :: - forall (t40 :: Row Type) (t41 :: Row Type) (t42 :: Type). - { inner :: { x :: (t42 :: Type) | (t40 :: Row Type) } | (t41 :: Row Type) } -> (t42 :: Type) + forall (t23 :: Row Type) (t24 :: Row Type) (t25 :: Type). + { inner :: { x :: (t25 :: Type) | (t23 :: Row Type) } | (t24 :: Row Type) } -> (t25 :: Type) Types diff --git a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap index 8465399f..af26c31b 100644 --- a/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap +++ b/tests-integration/fixtures/checking/226_record_binder_additional_property_nested/Main.snap @@ -10,7 +10,7 @@ Types Diagnostics error[AdditionalProperty]: Additional properties not allowed: y - --> 4:6..4:25 + --> 4:15..4:23 | 4 | test { outer: { x, y } } = x - | ^~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~ diff --git a/tests-integration/fixtures/checking/317_higher_rank_fields/Main.purs b/tests-integration/fixtures/checking/317_higher_rank_fields/Main.purs new file mode 100644 index 00000000..d88ac11b --- /dev/null +++ b/tests-integration/fixtures/checking/317_higher_rank_fields/Main.purs @@ -0,0 +1,9 @@ +module Main where + +type Test = { identity :: forall a. a -> a } + +test1 :: Test -> Int +test1 t = t.identity 42 + +test2 :: Test -> Int +test2 { identity } = identity 42 diff --git a/tests-integration/fixtures/checking/317_higher_rank_fields/Main.snap b/tests-integration/fixtures/checking/317_higher_rank_fields/Main.snap new file mode 100644 index 00000000..290639f6 --- /dev/null +++ b/tests-integration/fixtures/checking/317_higher_rank_fields/Main.snap @@ -0,0 +1,17 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test1 :: { identity :: forall (a :: Type). (a :: Type) -> (a :: Type) } -> Int +test2 :: { identity :: forall (a :: Type). (a :: Type) -> (a :: Type) } -> Int + +Types +Test :: Type + +Synonyms +Test = { identity :: forall (a :: Type). (a :: Type) -> (a :: Type) } + Quantified = :0 + Kind = :0 + Type = :0 diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index cea0926f..760feb05 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -645,3 +645,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_315_operator_chain_mixed_fixity_main() { run_test("315_operator_chain_mixed_fixity", "Main"); } #[rustfmt::skip] #[test] fn test_316_synonym_derive_main() { run_test("316_synonym_derive", "Main"); } + +#[rustfmt::skip] #[test] fn test_317_higher_rank_fields_main() { run_test("317_higher_rank_fields", "Main"); } From a33d61b145c89df10ff4ce2ef5c52d070f87726e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 10 Feb 2026 16:42:43 +0800 Subject: [PATCH 141/386] Add unification rule for Type::Forall --- .../checking/src/algorithm/unification.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 2195cf32..34b95a64 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -208,6 +208,20 @@ where unify(state, context, t1_left, t2_left)? && unify(state, context, t1_right, t2_right)? } + (Type::Forall(t1_binder, t1_inner), Type::Forall(t2_binder, t2_inner)) => { + let t1 = { + let v = Variable::Skolem(t1_binder.level, t1_binder.kind); + let t = state.storage.intern(Type::Variable(v)); + substitute::SubstituteBound::on(state, t1_binder.level, t, t1_inner) + }; + let t2 = { + let v = Variable::Skolem(t2_binder.level, t2_binder.kind); + let t = state.storage.intern(Type::Variable(v)); + substitute::SubstituteBound::on(state, t2_binder.level, t, t2_inner) + }; + unify(state, context, t1, t2)? + } + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { unify(state, context, t1_argument, t2_argument)? && unify(state, context, t1_result, t2_result)? From 4319bff86d421c43c4d0bbd5acd6906d38ee53b9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 10 Feb 2026 23:43:42 +0800 Subject: [PATCH 142/386] Migrate from level-based variables to unique-based --- Cargo.lock | 4 +- compiler-core/checking/src/algorithm.rs | 2 +- .../checking/src/algorithm/constraint.rs | 100 ++++++----- .../constraint/compiler_solved/prim_coerce.rs | 6 +- .../checking/src/algorithm/derive.rs | 6 +- .../checking/src/algorithm/derive/tools.rs | 12 +- .../checking/src/algorithm/derive/variance.rs | 60 +++---- .../checking/src/algorithm/equation.rs | 2 +- compiler-core/checking/src/algorithm/fold.rs | 8 +- .../checking/src/algorithm/inspect.rs | 79 ++++----- compiler-core/checking/src/algorithm/kind.rs | 44 ++--- .../checking/src/algorithm/kind/synonym.rs | 38 +---- .../checking/src/algorithm/quantify.rs | 92 +++++----- compiler-core/checking/src/algorithm/state.rs | 127 +++++++++++--- .../checking/src/algorithm/substitute.rs | 97 ++++------- compiler-core/checking/src/algorithm/term.rs | 10 +- .../checking/src/algorithm/term_item.rs | 38 +++-- .../checking/src/algorithm/toolkit.rs | 16 +- .../checking/src/algorithm/transfer.rs | 4 +- .../checking/src/algorithm/type_item.rs | 157 ++++++++++++------ .../checking/src/algorithm/unification.rs | 50 +++--- compiler-core/checking/src/core.rs | 49 +++++- compiler-core/checking/src/core/pretty.rs | 18 +- .../checking/010_class_basic/Main.snap | 3 +- .../checking/011_class_functor/Main.snap | 3 +- .../checking/012_class_monad_state/Main.snap | 5 +- .../checking/013_class_phantom/Main.snap | 3 +- .../014_class_with_signature/Main.snap | 3 +- .../checking/015_class_superclass/Main.snap | 5 +- .../checking/027_type_constrained/Main.snap | 3 +- .../076_inspect_constraints/Main.snap | 5 +- .../checking/083_instance_basic/Main.snap | 5 +- .../checking/084_instance_eq/Main.snap | 2 +- .../Main.snap | 3 +- .../Main.snap | 3 +- .../Main.snap | 3 +- .../088_given_constraint_matching/Main.snap | 3 +- .../checking/089_no_instance_found/Main.snap | 2 +- .../checking/090_instance_improve/Main.snap | 5 +- .../091_superclass_elaboration/Main.snap | 5 +- .../092_ambiguous_constraint/Main.snap | 4 +- .../093_constraint_generalization/Main.snap | 5 +- .../Main.snap | 2 +- .../095_given_constraint_arityless/Main.snap | 5 +- .../096_given_functional_dependency/Main.snap | 5 +- .../checking/097_instance_chains/Main.snap | 7 +- .../checking/098_fundep_propagation/Main.snap | 7 +- .../checking/102_builtin_row/Main.snap | 1 + .../checking/117_do_ado_constrained/Main.snap | 14 +- .../118_instance_member_type_match/Main.snap | 3 +- .../Main.snap | 2 +- .../Main.snap | 3 +- .../Main.snap | 3 +- .../Main.snap | 5 +- .../123_incomplete_instance_head/Main.snap | 2 +- .../Main.snap | 6 +- .../Main.snap | 2 +- .../checking/126_instance_phantom/Main.snap | 5 +- .../checking/127_derive_eq_simple/Main.snap | 2 +- .../130_derive_eq_parameterized/Main.snap | 3 +- .../132_derive_eq_1_higher_kinded/Main.snap | 6 +- .../checking/133_derive_eq_partial/Main.snap | 6 +- .../135_derive_ord_1_higher_kinded/Main.snap | 12 +- .../136_derive_nested_higher_kinded/Main.snap | 24 +-- .../139_derive_newtype_with_given/Main.snap | 3 +- .../140_derive_newtype_recursive/Main.snap | 3 +- .../141_derive_newtype_phantom/Main.snap | 3 +- .../Main.snap | 4 +- .../146_derive_functor_simple/Main.snap | 3 +- .../Main.snap | 6 +- .../Main.snap | 6 +- .../149_derive_bifunctor_simple/Main.snap | 3 +- .../Main.snap | 8 +- .../Main.snap | 2 +- .../152_derive_contravariant_simple/Main.snap | 3 +- .../153_derive_contravariant_error/Main.snap | 4 +- .../154_derive_profunctor_simple/Main.snap | 3 +- .../155_derive_profunctor_error/Main.snap | 6 +- .../158_derive_foldable_simple/Main.snap | 3 +- .../Main.snap | 6 +- .../160_derive_bifoldable_simple/Main.snap | 3 +- .../Main.snap | 8 +- .../162_derive_traversable_simple/Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 7 +- .../Main.snap | 6 +- .../checking/167_derive_eq_1/Main.snap | 22 +-- .../checking/168_derive_ord_1/Main.snap | 28 ++-- .../Main.snap | 3 +- .../172_derive_generic_simple/Main.snap | 8 +- .../Main.snap | 3 +- .../Main.snap | 3 +- .../Main.snap | 2 +- .../207_operator_class_method/Main.snap | 3 +- .../checking/208_int_add_constraint/Main.snap | 5 +- .../209_int_cons_constraint/Main.snap | 11 +- .../220_do_let_premature_solve/Main.snap | 2 +- .../222_ado_let_premature_solve/Main.snap | 2 +- .../Main.snap | 4 +- .../233_record_instance_matching/Main.snap | 8 +- .../234_record_instance_open_row/Main.snap | 8 +- .../235_instance_head_invalid_row/Main.snap | 2 +- .../236_category_function_instance/Main.snap | 4 +- .../237_bound_variable_unification/Main.snap | 4 +- .../Main.snap | 2 +- .../251_lookup_implicit_panic/Main.snap | 6 +- .../254_higher_rank_elaboration/Main.snap | 2 +- .../Main.snap | 2 +- .../273_class_member_instantiation/Main.snap | 4 +- .../checking/274_givens_retained/Main.snap | 2 +- .../checking/275_givens_scoped/Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 2 +- .../Main.snap | 4 +- .../Main.snap | 8 +- .../300_instance_shift_variables/Main.snap | 2 +- .../303_instance_given_constraint/Main.snap | 4 +- .../Main.snap | 2 +- .../307_where_let_interaction/Main.snap | 2 +- .../308_let_constraint_scoping/Main.snap | 2 +- .../Main.snap | 2 +- tests-integration/src/generated/basic.rs | 19 ++- tests-integration/tests/checking.rs | 132 +++++++++++---- tests-integration/tests/checking/generated.rs | 8 + .../snapshots/checking__solve_bound.snap | 3 +- 125 files changed, 950 insertions(+), 702 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70791449..36036b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2337,9 +2337,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smol_str" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" dependencies = [ "borsh", "serde_core", diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 29844940..72ca605c 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -81,7 +81,7 @@ use crate::core::{Role, Type, TypeId}; use crate::{CheckedModule, ExternalQueries}; pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { - let mut state = state::CheckState::default(); + let mut state = state::CheckState::new(file_id); let context = state::CheckContext::new(queries, &mut state, file_id)?; check_type_signatures(&mut state, &context)?; diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 9dfd7d02..e22e3928 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -23,8 +23,8 @@ use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::visit::{ CollectFileReferences, HasLabeledRole, TypeVisitor, VisitAction, visit_type, }; -use crate::algorithm::{toolkit, transfer, unification}; -use crate::core::{self, Class, Instance, InstanceKind, Variable, debruijn}; +use crate::algorithm::{substitute, toolkit, transfer, unification}; +use crate::core::{self, Class, Instance, InstanceKind, Name, Variable}; use crate::{CheckedModule, ExternalQueries, Type, TypeId}; #[tracing::instrument(skip_all, name = "solve_constraints")] @@ -286,11 +286,9 @@ where return Ok(()); } - let initial_level = class_info.quantified_variables.0 + class_info.kind_variables.0; - let mut bindings = FxHashMap::default(); - for (index, &argument) in arguments.iter().enumerate() { - let level = debruijn::Level(initial_level + index as u32); - bindings.insert(level, argument); + let mut bindings: substitute::NameToType = FxHashMap::default(); + for (name, &argument) in class_info.type_variable_names.iter().zip(arguments.iter()) { + bindings.insert(name.clone(), argument); } for &(superclass, _) in class_info.superclasses.iter() { @@ -389,6 +387,7 @@ where Class { superclasses, type_variable_kinds, + type_variable_names: class.type_variable_names.clone(), quantified_variables: class.quantified_variables, kind_variables: class.kind_variables, } @@ -555,7 +554,7 @@ enum MatchInstance { fn match_type( state: &mut CheckState, context: &CheckContext, - bindings: &mut FxHashMap, + bindings: &mut FxHashMap, equalities: &mut Vec<(TypeId, TypeId)>, wanted: TypeId, given: TypeId, @@ -574,8 +573,8 @@ where let given_core = &state.storage[given]; match (wanted_core, given_core) { - (_, Type::Variable(Variable::Bound(level, _))) => { - if let Some(&bound) = bindings.get(level) { + (_, Type::Variable(Variable::Bound(name, _))) => { + if let Some(&bound) = bindings.get(name) { match can_unify(state, wanted, bound) { CanUnify::Equal => MatchType::Match, CanUnify::Apart => MatchType::Apart, @@ -585,7 +584,7 @@ where } } } else { - bindings.insert(*level, wanted); + bindings.insert(name.clone(), wanted); MatchType::Match } } @@ -664,7 +663,7 @@ where fn match_row_type( state: &mut CheckState, context: &CheckContext, - bindings: &mut FxHashMap, + bindings: &mut FxHashMap, equalities: &mut Vec<(TypeId, TypeId)>, wanted_row: core::RowType, given_row: core::RowType, @@ -783,10 +782,10 @@ where (Type::Unification(_), _) => MatchType::Stuck, ( - Type::Variable(Variable::Bound(w_level, w_kind)), - Type::Variable(Variable::Bound(g_level, g_kind)), + Type::Variable(Variable::Bound(w_name, w_kind)), + Type::Variable(Variable::Bound(g_name, g_kind)), ) => { - if w_level == g_level { + if w_name == g_name { match_given_type(state, context, *w_kind, *g_kind) } else { MatchType::Apart @@ -794,10 +793,10 @@ where } ( - Type::Variable(Variable::Skolem(w_level, w_kind)), - Type::Variable(Variable::Skolem(g_level, g_kind)), + Type::Variable(Variable::Skolem(w_name, w_kind)), + Type::Variable(Variable::Skolem(g_name, g_kind)), ) => { - if w_level == g_level { + if w_name == g_name { match_given_type(state, context, *w_kind, *g_kind) } else { MatchType::Apart @@ -921,8 +920,9 @@ fn can_unify(state: &mut CheckState, t1: TypeId, t2: TypeId) -> CanUnify { ) => can_unify(state, t1_constraint, t2_constraint) .and_also(|| can_unify(state, t1_body, t2_body)), - (&Type::Forall(_, t1_body), &Type::Forall(_, t2_body)) => { - can_unify(state, t1_body, t2_body) + (&Type::Forall(ref t1_binder, t1_body), &Type::Forall(ref t2_binder, t2_body)) => { + can_unify(state, t1_binder.kind, t2_binder.kind) + .and_also(|| can_unify(state, t1_body, t2_body)) } ( @@ -956,10 +956,10 @@ fn can_unify(state: &mut CheckState, t1: TypeId, t2: TypeId) -> CanUnify { } ( - &Type::Variable(Variable::Bound(t1_level, t1_kind)), - &Type::Variable(Variable::Bound(t2_level, t2_kind)), + &Type::Variable(Variable::Bound(ref t1_name, t1_kind)), + &Type::Variable(Variable::Bound(ref t2_name, t2_kind)), ) => { - if t1_level == t2_level { + if t1_name == t2_name { can_unify(state, t1_kind, t2_kind) } else { Apart @@ -967,10 +967,10 @@ fn can_unify(state: &mut CheckState, t1: TypeId, t2: TypeId) -> CanUnify { } ( - &Type::Variable(Variable::Skolem(t1_level, t1_kind)), - &Type::Variable(Variable::Skolem(t2_level, t2_kind)), + &Type::Variable(Variable::Skolem(ref t1_name, t1_kind)), + &Type::Variable(Variable::Skolem(ref t2_name, t2_kind)), ) => { - if t1_level == t2_level { + if t1_name == t2_name { can_unify(state, t1_kind, t2_kind) } else { Apart @@ -1030,10 +1030,10 @@ where } } - let mut argument_levels = FxHashSet::default(); + let mut argument_names = FxHashSet::default(); for &(argument, _) in &instance.arguments { let localized = transfer::localize(state, context, argument); - CollectBoundLevels::on(state, localized, &mut argument_levels); + CollectBoundNames::on(state, localized, &mut argument_names); } let mut constraint_variables = FxHashMap::default(); @@ -1042,10 +1042,10 @@ where CollectBoundVariables::on(state, localized, &mut constraint_variables); } - for (level, kind) in constraint_variables { - if !argument_levels.contains(&level) && !bindings.contains_key(&level) { + for (name, kind) in constraint_variables { + if !argument_names.contains(&name) && !bindings.contains_key(&name) { let unification = state.fresh_unification_kinded(kind); - bindings.insert(level, unification); + bindings.insert(name, unification); } } @@ -1186,13 +1186,13 @@ where } struct ApplyBindings<'a> { - bindings: &'a FxHashMap, + bindings: &'a FxHashMap, } impl<'a> ApplyBindings<'a> { fn on( state: &mut CheckState, - bindings: &'a FxHashMap, + bindings: &'a FxHashMap, type_id: TypeId, ) -> TypeId { fold_type(state, type_id, &mut ApplyBindings { bindings }) @@ -1202,8 +1202,8 @@ impl<'a> ApplyBindings<'a> { impl TypeFold for ApplyBindings<'_> { fn transform(&mut self, _state: &mut CheckState, id: TypeId, t: &Type) -> FoldAction { match t { - Type::Variable(Variable::Bound(level, _)) => { - let id = self.bindings.get(level).copied().unwrap_or(id); + Type::Variable(Variable::Bound(name, _)) => { + let id = self.bindings.get(name).copied().unwrap_or(id); FoldAction::Replace(id) } _ => FoldAction::Continue, @@ -1211,21 +1211,21 @@ impl TypeFold for ApplyBindings<'_> { } } -/// Collects all bound variable levels from a type. -struct CollectBoundLevels<'a> { - levels: &'a mut FxHashSet, +/// Collects all bound variable names from a type. +struct CollectBoundNames<'a> { + names: &'a mut FxHashSet, } -impl<'a> CollectBoundLevels<'a> { - fn on(state: &mut CheckState, type_id: TypeId, levels: &'a mut FxHashSet) { - visit_type(state, type_id, &mut CollectBoundLevels { levels }); +impl<'a> CollectBoundNames<'a> { + fn on(state: &mut CheckState, type_id: TypeId, names: &'a mut FxHashSet) { + visit_type(state, type_id, &mut CollectBoundNames { names }); } } -impl TypeVisitor for CollectBoundLevels<'_> { +impl TypeVisitor for CollectBoundNames<'_> { fn visit(&mut self, _state: &mut CheckState, _id: TypeId, t: &Type) -> VisitAction { - if let Type::Variable(Variable::Bound(level, _)) = t { - self.levels.insert(*level); + if let Type::Variable(Variable::Bound(name, _)) = t { + self.names.insert(name.clone()); } VisitAction::Continue } @@ -1233,23 +1233,19 @@ impl TypeVisitor for CollectBoundLevels<'_> { /// Collects all bound variables with their kinds from a type. struct CollectBoundVariables<'a> { - variables: &'a mut FxHashMap, + variables: &'a mut FxHashMap, } impl<'a> CollectBoundVariables<'a> { - fn on( - state: &mut CheckState, - type_id: TypeId, - variables: &'a mut FxHashMap, - ) { + fn on(state: &mut CheckState, type_id: TypeId, variables: &'a mut FxHashMap) { visit_type(state, type_id, &mut CollectBoundVariables { variables }); } } impl TypeVisitor for CollectBoundVariables<'_> { fn visit(&mut self, _state: &mut CheckState, _id: TypeId, t: &Type) -> VisitAction { - if let Type::Variable(Variable::Bound(level, kind)) = t { - self.variables.insert(*level, *kind); + if let Type::Variable(Variable::Bound(name, kind)) = t { + self.variables.insert(name.clone(), *kind); } VisitAction::Continue } diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index 9455b89a..3ba0ab82 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -412,15 +412,15 @@ fn decompose_kind_for_coercion( kind_id = state.normalize_type(kind_id); let forall = match &state.storage[kind_id] { - Type::Forall(binder, inner) => Some((binder.kind, binder.level, *inner)), + Type::Forall(binder, inner) => Some((binder.kind, binder.variable.clone(), *inner)), Type::Function(domain, _) => return Some((type_id, *domain)), _ => return None, }; - if let Some((binder_kind, binder_level, inner_kind)) = forall { + if let Some((binder_kind, binder_variable, inner_kind)) = forall { let fresh_kind = state.fresh_skolem_kinded(binder_kind); type_id = state.storage.intern(Type::KindApplication(type_id, fresh_kind)); - kind_id = substitute::SubstituteBound::on(state, binder_level, fresh_kind, inner_kind); + kind_id = substitute::SubstituteBound::on(state, binder_variable, fresh_kind, inner_kind); } } } diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index fd143da5..be111ee0 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -387,11 +387,7 @@ fn generate_delegate_constraint( class: (FileId, TypeItemId), ) { // Introduce a fresh skolem `~a` for the last type parameter. - let skolem_level = state.type_scope.size().0; - let skolem_level = debruijn::Level(skolem_level); - - let skolem_type = Variable::Skolem(skolem_level, prim_type); - let skolem_type = state.storage.intern(Type::Variable(skolem_type)); + let skolem_type = state.fresh_skolem_kinded(prim_type); // Given `Eq ~a`, prove `Eq (Identity ~a)`. let applied_type = state.storage.intern(Type::Application(derived_type, skolem_type)); diff --git a/compiler-core/checking/src/algorithm/derive/tools.rs b/compiler-core/checking/src/algorithm/derive/tools.rs index 058c77f8..b524f705 100644 --- a/compiler-core/checking/src/algorithm/derive/tools.rs +++ b/compiler-core/checking/src/algorithm/derive/tools.rs @@ -10,11 +10,11 @@ use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{constraint, quantify, transfer}; -use crate::core::{Instance, InstanceKind, Type, TypeId, debruijn}; +use crate::core::{Instance, InstanceKind, Type, TypeId}; use crate::error::ErrorKind; mod substitute { - pub use crate::algorithm::substitute::SubstituteBindings; + pub use crate::algorithm::substitute::{NameToType, SubstituteBindings}; } /// Elaborated derive instance after kind inference. @@ -64,11 +64,9 @@ where return Ok(()); } - let initial_level = class_info.quantified_variables.0 + class_info.kind_variables.0; - let mut bindings = FxHashMap::default(); - for (index, &(argument_type, _)) in arguments.iter().enumerate() { - let level = debruijn::Level(initial_level + index as u32); - bindings.insert(level, argument_type); + let mut bindings: substitute::NameToType = FxHashMap::default(); + for (name, &(argument_type, _)) in class_info.type_variable_names.iter().zip(arguments) { + bindings.insert(name.clone(), argument_type); } for &(superclass, _) in class_info.superclasses.iter() { diff --git a/compiler-core/checking/src/algorithm/derive/variance.rs b/compiler-core/checking/src/algorithm/derive/variance.rs index 01520344..44211b38 100644 --- a/compiler-core/checking/src/algorithm/derive/variance.rs +++ b/compiler-core/checking/src/algorithm/derive/variance.rs @@ -12,7 +12,7 @@ use crate::algorithm::derive::{self, tools}; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{substitute, toolkit}; -use crate::core::{RowType, Type, TypeId, Variable, debruijn}; +use crate::core::{Name, RowType, Type, TypeId, Variable}; use crate::error::ErrorKind; /// Variance of a type position. @@ -32,9 +32,9 @@ impl Variance { } /// A derived type parameter with its expected variance and wrapper class. -#[derive(Clone, Copy)] +#[derive(Clone)] struct DerivedParameter { - level: debruijn::Level, + name: Name, /// Expected variance for this parameter. expected: Variance, /// The class to emit when this parameter appears wrapped in a type application. @@ -42,8 +42,8 @@ struct DerivedParameter { } impl DerivedParameter { - fn new(level: debruijn::Level, (expected, class): ParameterConfig) -> DerivedParameter { - DerivedParameter { level, expected, class } + fn new(name: Name, (expected, class): ParameterConfig) -> DerivedParameter { + DerivedParameter { name, expected, class } } } @@ -59,8 +59,8 @@ enum DerivedSkolems { } impl DerivedSkolems { - fn get(&self, level: debruijn::Level) -> Option<&DerivedParameter> { - self.iter().find(|p| p.level == level) + fn get(&self, name: &Name) -> Option<&DerivedParameter> { + self.iter().find(|p| p.name == *name) } fn iter(&self) -> impl Iterator { @@ -133,36 +133,36 @@ where let type_arguments = toolkit::extract_all_applications(state, derived_type); let mut arguments_iter = type_arguments.into_iter(); let mut current_id = constructor_type; - let mut levels = vec![]; + let mut names = vec![]; safe_loop! { current_id = state.normalize_type(current_id); match &state.storage[current_id] { Type::Forall(binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let inner = *inner; let argument_type = arguments_iter.next().unwrap_or_else(|| { - levels.push(binder_level); - let skolem = Variable::Skolem(binder_level, binder_kind); + names.push(binder_variable.clone()); + let skolem = Variable::Skolem(binder_variable.clone(), binder_kind); state.storage.intern(Type::Variable(skolem)) }); - current_id = substitute::SubstituteBound::on(state, binder_level, argument_type, inner); + current_id = substitute::SubstituteBound::on(state, binder_variable, argument_type, inner); } _ => break, } } - // The last N levels correspond to the N derived parameters. - let skolems = match (config, &levels[..]) { + // The last N names correspond to the N derived parameters. + let skolems = match (config, &names[..]) { (VarianceConfig::Single(config), [.., a]) => { - DerivedSkolems::Single(DerivedParameter::new(*a, *config)) + DerivedSkolems::Single(DerivedParameter::new(a.clone(), *config)) } (VarianceConfig::Pair(a_config, b_config), [.., a, b]) => DerivedSkolems::Pair( - DerivedParameter::new(*a, *a_config), - DerivedParameter::new(*b, *b_config), + DerivedParameter::new(a.clone(), *a_config), + DerivedParameter::new(b.clone(), *b_config), ), _ => { let type_message = state.render_local_type(context, derived_type); @@ -199,8 +199,8 @@ fn check_variance_field( let type_id = state.normalize_type(type_id); match state.storage[type_id].clone() { - Type::Variable(Variable::Skolem(level, _)) => { - if let Some(parameter) = skolems.get(level) + Type::Variable(Variable::Skolem(name, _)) => { + if let Some(parameter) = skolems.get(&name) && variance != parameter.expected { let type_message = state.render_local_type(context, type_id); @@ -224,7 +224,7 @@ fn check_variance_field( check_variance_field(state, context, argument, variance, skolems); } else { for parameter in skolems.iter() { - if contains_skolem_level(state, argument, parameter.level) { + if contains_skolem_name(state, argument, ¶meter.name) { if variance != parameter.expected { let type_message = state.render_local_type(context, type_id); if variance == Variance::Covariant { @@ -259,30 +259,30 @@ fn check_variance_field( } } -/// Checks if a type contains a specific Skolem level. -fn contains_skolem_level(state: &mut CheckState, type_id: TypeId, target: debruijn::Level) -> bool { +/// Checks if a type contains a specific Skolem name. +fn contains_skolem_name(state: &mut CheckState, type_id: TypeId, target: &Name) -> bool { let type_id = state.normalize_type(type_id); match state.storage[type_id].clone() { - Type::Variable(Variable::Skolem(level, _)) => level == target, + Type::Variable(Variable::Skolem(name, _)) => name == *target, Type::Application(function, argument) | Type::KindApplication(function, argument) => { - contains_skolem_level(state, function, target) - || contains_skolem_level(state, argument, target) + contains_skolem_name(state, function, target) + || contains_skolem_name(state, argument, target) } Type::Function(argument, result) => { - contains_skolem_level(state, argument, target) - || contains_skolem_level(state, result, target) + contains_skolem_name(state, argument, target) + || contains_skolem_name(state, result, target) } Type::Row(RowType { ref fields, tail }) => { - fields.iter().any(|f| contains_skolem_level(state, f.id, target)) - || tail.is_some_and(|t| contains_skolem_level(state, t, target)) + fields.iter().any(|f| contains_skolem_name(state, f.id, target)) + || tail.is_some_and(|t| contains_skolem_name(state, t, target)) } Type::Forall(_, inner) | Type::Constrained(_, inner) | Type::Kinded(inner, _) => { - contains_skolem_level(state, inner, target) + contains_skolem_name(state, inner, target) } _ => false, diff --git a/compiler-core/checking/src/algorithm/equation.rs b/compiler-core/checking/src/algorithm/equation.rs index 7a047f69..96059abe 100644 --- a/compiler-core/checking/src/algorithm/equation.rs +++ b/compiler-core/checking/src/algorithm/equation.rs @@ -182,7 +182,7 @@ where let _ = constraints(state, context, ConstraintsPolicy::Report)?; if let Some(variable) = signature.variables.first() { - state.type_scope.unbind(variable.level); + state.type_scope.unbind_name(&variable.variable); } Ok(()) diff --git a/compiler-core/checking/src/algorithm/fold.rs b/compiler-core/checking/src/algorithm/fold.rs index 6895577a..161f0d26 100644 --- a/compiler-core/checking/src/algorithm/fold.rs +++ b/compiler-core/checking/src/algorithm/fold.rs @@ -104,13 +104,13 @@ pub fn fold_type(state: &mut CheckState, id: TypeId, folder: &mut F } Type::Unification(_) => id, Type::Variable(variable) => match variable { - Variable::Bound(level, kind) => { + Variable::Bound(name, kind) => { let kind = fold_type(state, kind, folder); - state.storage.intern(Type::Variable(Variable::Bound(level, kind))) + state.storage.intern(Type::Variable(Variable::Bound(name, kind))) } - Variable::Skolem(level, kind) => { + Variable::Skolem(name, kind) => { let kind = fold_type(state, kind, folder); - state.storage.intern(Type::Variable(Variable::Skolem(level, kind))) + state.storage.intern(Type::Variable(Variable::Skolem(name, kind))) } Variable::Free(_) => id, }, diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index fb870a1d..230dd5ee 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -7,7 +7,7 @@ use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{normalise, substitute}; -use crate::core::{ForallBinder, Type, TypeId, Variable, debruijn}; +use crate::core::{ForallBinder, Type, TypeId, Variable}; pub struct InspectSignature { pub variables: Vec, @@ -68,32 +68,28 @@ where // // transform :: forall f g. NaturalTransformation f g // - // With De Bruijn levels, `transform`'s type becomes: - // - // transform :: forall f:0 g:1. NaturalTransformation f g - // // Synonym expansion can reveal additional quantifiers: // - // transform :: forall f:0 g:1. forall a:2. f a -> g a + // transform :: forall f g. forall a. f a -> g a // - // The following algorithm is designed to handle level - // adjustments relative to the current scope level. + // The following algorithm rebinds each quantifier's variable + // name to a fresh name in the current scope. let mut surface_bindings = surface_bindings.iter(); let mut variables = vec![]; let mut current_id = type_id; - let mut bindings = substitute::LevelToType::default(); + let mut bindings = substitute::NameToType::default(); safe_loop! { current_id = normalise::normalise_expand_type(state, context, current_id)?; - // In the example, after the Forall case has peeled f:0, g:1, and the - // synonym-expanded a:2, the accumulated bindings are the following: + // In the example, after the Forall case has peeled f, g, and the + // synonym-expanded a, the accumulated bindings are the following: // - // { 0 -> f', 1 -> g', 2 -> 'a } + // { old_f -> f', old_g -> g', old_a -> a' } // - // We're at a monomorphic type at this point, f:0 a:2 -> g:1 a:2, so - // we can now proceed with applying the substitutions and continuing. + // We're at a monomorphic type at this point, so we can now proceed + // with applying the substitutions and continuing. if !matches!(state.storage[current_id], Type::Forall(..)) && !bindings.is_empty() { current_id = substitute::SubstituteBindings::on(state, &bindings, current_id); bindings.clear(); @@ -101,31 +97,32 @@ where } match state.storage[current_id] { - // Bind each ForallBinder relative to the current scope, recording the - // level substitution for later. In the example, this accumulates the - // following substitutions before we hit the Function type: - // - // { 0 -> f', 1 -> g', 2 -> 'a } - // + // Bind each ForallBinder relative to the current scope, recording + // the name substitution for later. Type::Forall(ref binder, inner) => { let mut binder = ForallBinder::clone(binder); - let new_level = if !binder.implicit - && let Some(&binding_id) = surface_bindings.next() - { - state.type_scope.bound.bind(debruijn::Variable::Forall(binding_id)) + let old_name = binder.variable.clone(); + let new_name = state.fresh_name(&binder.text); + + if !binder.implicit && let Some(&binding_id) = surface_bindings.next() { + state.type_scope.bind_forall(binding_id, binder.kind, new_name.clone()); } else { - state.type_scope.bound.bind(debruijn::Variable::Core) - }; + state.type_scope.bind_core(binder.kind, new_name.clone()); + } - let old_level = binder.level; - binder.level = new_level; - state.type_scope.kinds.insert(new_level, binder.kind); + binder.variable = new_name.clone(); - let variable = Type::Variable(Variable::Bound(new_level, binder.kind)); + // Substitute the binder's kind through existing bindings so that + // references to earlier forall variables use the fresh Names. + if !bindings.is_empty() { + binder.kind = substitute::SubstituteBindings::on(state, &bindings, binder.kind); + } + + let variable = Type::Variable(Variable::Bound(new_name, binder.kind)); let variable = state.storage.intern(variable); - bindings.insert(old_level, variable); + bindings.insert(old_name, variable); variables.push(binder); current_id = inner; } @@ -153,7 +150,7 @@ where { let mut arguments = vec![]; let mut current_id = type_id; - let mut bindings = substitute::LevelToType::default(); + let mut bindings = substitute::NameToType::default(); safe_loop! { current_id = normalise::normalise_expand_type(state, context, current_id)?; @@ -166,16 +163,22 @@ where match state.storage[current_id] { Type::Forall(ref binder, inner) => { - let old_level = binder.level; - let kind = binder.kind; + let old_name = binder.variable.clone(); + let mut kind = binder.kind; + + let text = binder.text.clone(); + let name = state.fresh_name(&text); - let new_level = state.type_scope.bound.bind(debruijn::Variable::Core); - state.type_scope.kinds.insert(new_level, kind); + state.type_scope.bind_core(kind, name.clone()); + + if !bindings.is_empty() { + kind = substitute::SubstituteBindings::on(state, &bindings, kind); + } - let variable = Type::Variable(Variable::Bound(new_level, kind)); + let variable = Type::Variable(Variable::Bound(name, kind)); let variable = state.storage.intern(variable); - bindings.insert(old_level, variable); + bindings.insert(old_name, variable); current_id = inner; } diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index 71004671..3f295770 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -13,7 +13,7 @@ use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{substitute, transfer, unification}; -use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, Variable}; +use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; const MISSING_NAME: SmolStr = SmolStr::new_static(""); @@ -134,6 +134,8 @@ where } lowering::TypeKind::Forall { bindings, inner } => { + let unbind_level = debruijn::Level(state.type_scope.size().0); + let binders = bindings .iter() .map(|binding| check_type_variable_binding(state, context, binding)) @@ -155,8 +157,8 @@ where let k = context.prim.t; - if let Some(binder) = binders.first() { - state.type_scope.unbind(binder.level); + if !binders.is_empty() { + state.type_scope.unbind(unbind_level); } Ok((t, k)) @@ -215,7 +217,8 @@ where } Some(lowering::TypeVariableResolution::Implicit(implicit)) => { - Ok(infer_implicit_variable(state, context, implicit)) + let text = name.clone().unwrap_or(MISSING_NAME); + Ok(infer_implicit_variable(state, context, implicit, text)) } None => { @@ -257,14 +260,14 @@ fn infer_forall_variable( state: &mut CheckState, forall: TypeVariableBindingId, ) -> (TypeId, TypeId) { - let level = + let name = state.type_scope.lookup_forall(forall).expect("invariant violated: TypeScope::bind_forall"); let k = state .type_scope .lookup_forall_kind(forall) .expect("invariant violated: TypeScope::bind_forall"); - let variable = Variable::Bound(level, k); + let variable = Variable::Bound(name, k); let t = state.storage.intern(Type::Variable(variable)); (t, k) @@ -274,16 +277,18 @@ fn infer_implicit_variable( state: &mut CheckState, context: &CheckContext, implicit: &lowering::ImplicitTypeVariable, + text: SmolStr, ) -> (TypeId, TypeId) { let (t, k) = if implicit.binding { let kind = state.fresh_unification(context); + let name = state.fresh_name(&text); - let level = state.type_scope.bind_implicit(implicit.node, implicit.id, kind); - let variable = Variable::Bound(level, kind); + let name = state.type_scope.bind_implicit(implicit.node, implicit.id, kind, name); + let variable = Variable::Bound(name, kind); (state.storage.intern(Type::Variable(variable)), kind) } else { - let level = state + let name = state .type_scope .lookup_implicit(implicit.node, implicit.id) .expect("invariant violated: TypeScope::bind_implicit"); @@ -292,7 +297,7 @@ fn infer_implicit_variable( .lookup_implicit_kind(implicit.node, implicit.id) .expect("invariant violated: TypeScope::bind_implicit"); - let variable = Variable::Bound(level, kind); + let variable = Variable::Bound(name, kind); (state.storage.intern(Type::Variable(variable)), kind) }; @@ -388,14 +393,14 @@ where } Type::Forall(ref binder, function_k) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let k = state.normalize_type(binder_kind); let t = state.fresh_unification_kinded(k); let function_t = state.storage.intern(Type::KindApplication(function_t, t)); - let function_k = substitute::SubstituteBound::on(state, binder_level, t, function_k); + let function_k = substitute::SubstituteBound::on(state, binder_variable, t, function_k); infer_surface_app_kind(state, context, (function_t, function_k), argument) } @@ -472,9 +477,9 @@ where let function_kind = state.normalize_type(function_kind); match state.storage[function_kind] { Type::Forall(ref binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let argument = state.normalize_type(argument); - substitute::SubstituteBound::on(state, binder_level, argument, inner) + substitute::SubstituteBound::on(state, binder_variable, argument, inner) } _ => unknown, } @@ -578,12 +583,12 @@ fn instantiate_kind_applications( break; }; - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = state.normalize_type(binder.kind); let argument_type = state.fresh_unification_kinded(binder_kind); t = state.storage.intern(Type::KindApplication(t, argument_type)); - k = substitute::SubstituteBound::on(state, binder_level, argument_type, inner_kind); + k = substitute::SubstituteBound::on(state, binder_variable, argument_type, inner_kind); } (t, k) @@ -628,7 +633,7 @@ where Q: ExternalQueries, { let visible = binding.visible; - let name = binding.name.clone().unwrap_or(MISSING_NAME); + let text = binding.name.clone().unwrap_or(MISSING_NAME); let kind = if let Some(id) = binding.kind { let (kind, _) = check_surface_kind(state, context, id, context.prim.t)?; @@ -637,8 +642,9 @@ where state.fresh_unification_type(context) }; - let level = state.type_scope.bind_forall(binding.id, kind); - Ok(ForallBinder { visible, implicit: false, name, level, kind }) + let name = state.fresh_name(&text); + state.type_scope.bind_forall(binding.id, kind, name.clone()); + Ok(ForallBinder { visible, implicit: false, text, variable: name, kind }) } pub(crate) fn lookup_file_type( diff --git a/compiler-core/checking/src/algorithm/kind/synonym.rs b/compiler-core/checking/src/algorithm/kind/synonym.rs index cac0962f..6c00f95f 100644 --- a/compiler-core/checking/src/algorithm/kind/synonym.rs +++ b/compiler-core/checking/src/algorithm/kind/synonym.rs @@ -16,7 +16,7 @@ use lowering::GroupedModule; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{kind, substitute, transfer, unification}; -use crate::core::{Saturation, Synonym, Type, TypeId, debruijn}; +use crate::core::{Saturation, Synonym, Type, TypeId}; use crate::error::ErrorKind; use crate::{CheckedModule, ExternalQueries}; @@ -314,13 +314,13 @@ where } Type::Forall(ref binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let k = state.normalize_type(binder_kind); let t = state.fresh_unification_kinded(k); - let function_k = substitute::SubstituteBound::on(state, binder_level, t, inner); + let function_k = substitute::SubstituteBound::on(state, binder_variable, t, inner); infer_synonym_application_argument(state, context, function_k, argument) } @@ -497,37 +497,14 @@ fn instantiate_saturated(state: &mut CheckState, synonym: Synonym, arguments: &[ let count = synonym.quantified_variables.0 as usize + synonym.kind_variables.0 as usize; let mut instantiated = state.normalize_type(synonym.synonym_type); - // Synonym bodies are originally bound starting at level 0. When expanding - // in a non-empty scope, we shift their levels up by the current scope size - // size to avoid conflicts. - // - // First, without shifting - // - // ```purescript - // type Transform f:0 g:1 = forall a:2. f a -> g a - // - // instance Parallel (ReaderT e:0 f:1) (ReaderT e:0 m:2) where - // -- parallel :: m:2 ~> f:1 - // -- :: forall a:2. m a -> f a - // ``` - // - // Then, with shifting - // - // ```purescript - // instance Parallel (ReaderT e:0 f:1) (ReaderT e:0 m:2) where - // -- parallel :: m:2 ~> f:1 - // -- :: forall a:5. m a -> f a - // ``` - let debruijn::Size(scope_size) = state.type_scope.size(); - instantiated = substitute::ShiftBound::on(state, instantiated, scope_size); - for _ in 0..count { if let Type::Forall(ref binder, inner) = state.storage[instantiated] { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let unification = state.fresh_unification_kinded(binder_kind); - instantiated = substitute::SubstituteBound::on(state, binder_level, unification, inner); + instantiated = + substitute::SubstituteBound::on(state, binder_variable, unification, inner); } else { break; } @@ -535,7 +512,8 @@ fn instantiate_saturated(state: &mut CheckState, synonym: Synonym, arguments: &[ for &argument in arguments { if let Type::Forall(ref binder, inner) = state.storage[instantiated] { - instantiated = substitute::SubstituteBound::on(state, binder.level, argument, inner); + instantiated = + substitute::SubstituteBound::on(state, binder.variable.clone(), argument, inner); } else { break; } diff --git a/compiler-core/checking/src/algorithm/quantify.rs b/compiler-core/checking/src/algorithm/quantify.rs index 629759fb..59e967c9 100644 --- a/compiler-core/checking/src/algorithm/quantify.rs +++ b/compiler-core/checking/src/algorithm/quantify.rs @@ -12,7 +12,7 @@ use smol_str::SmolStrBuilder; use crate::algorithm::constraint::{self, ConstraintApplication}; use crate::algorithm::fold::Zonk; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::substitute::{ShiftBound, SubstituteUnification, UniToLevel}; +use crate::algorithm::substitute::{SubstituteUnification, UniToName}; use crate::core::{Class, ForallBinder, Instance, RowType, Type, TypeId, Variable, debruijn}; use crate::{ExternalQueries, safe_loop}; @@ -30,24 +30,20 @@ pub fn quantify(state: &mut CheckState, id: TypeId) -> Option<(TypeId, debruijn: debruijn::Size(size as u32) }; - // Shift existing bound variable levels to make room for new quantifiers - let mut quantified = ShiftBound::on(state, id, size.0); - let mut substitutions = UniToLevel::default(); + let mut quantified = id; + let mut substitutions = UniToName::default(); - for (index, &id) in unsolved.iter().rev().enumerate() { + for &id in unsolved.iter().rev() { let kind = state.unification.get(id).kind; - let kind = ShiftBound::on(state, kind, size.0); - let name = generate_type_name(id); + let text = generate_type_name(id); + let mut name = state.fresh_name(&text); + name.depth = debruijn::Size(0); - let index = debruijn::Index(index as u32); - let level = index - .to_level(size) - .unwrap_or_else(|| unreachable!("invariant violated: invalid {index} for {size}")); - - let binder = ForallBinder { visible: false, implicit: true, name, level, kind }; + let binder = + ForallBinder { visible: false, implicit: true, text, variable: name.clone(), kind }; quantified = state.storage.intern(Type::Forall(binder, quantified)); - substitutions.insert(id, (level, kind)); + substitutions.insert(id, (name, kind)); } let quantified = SubstituteUnification::on(&substitutions, state, quantified); @@ -228,7 +224,7 @@ fn classify_constraints_by_reachability( } } -fn generate_type_name(id: u32) -> smol_str::SmolStr { +pub(crate) fn generate_type_name(id: u32) -> smol_str::SmolStr { let mut builder = SmolStrBuilder::default(); write!(builder, "t{id}").unwrap(); builder.finish() @@ -259,26 +255,24 @@ pub fn quantify_class(state: &mut CheckState, class: &mut Class) -> Option Opt } let unsolved = ordered_toposort(&graph, state)?; - let size = debruijn::Size(unsolved.len() as u32); - let mut substitutions = UniToLevel::default(); - for (index, &id) in unsolved.iter().rev().enumerate() { + let mut substitutions = UniToName::default(); + for &id in unsolved.iter().rev() { let kind = state.unification.get(id).kind; - let kind = ShiftBound::on(state, kind, size.0); - let index = debruijn::Index(index as u32); - let level = index.to_level(size)?; - substitutions.insert(id, (level, kind)); + let text = generate_type_name(id); + let mut name = state.fresh_name(&text); + name.depth = debruijn::Size(0); + substitutions.insert(id, (name, kind)); } - let kind_variables = substitutions.values().copied(); - let kind_variables = kind_variables.sorted_by_key(|(level, _)| *level); - let kind_variables = kind_variables.map(|(_, kind)| kind).collect_vec(); + let kind_variables = substitutions.values().cloned(); + let kind_variables = kind_variables.sorted_by_key(|(name, _)| name.unique); + let kind_variables = kind_variables.map(|(name, kind)| (name, kind)).collect_vec(); let arguments = instance.arguments.iter().map(|&(t, k)| { - let t = ShiftBound::on(state, t, size.0); let t = SubstituteUnification::on(&substitutions, state, t); - let k = ShiftBound::on(state, k, size.0); let k = SubstituteUnification::on(&substitutions, state, k); (t, k) }); @@ -339,9 +330,7 @@ pub fn quantify_instance(state: &mut CheckState, instance: &mut Instance) -> Opt instance.arguments = arguments.collect(); let constraints = instance.constraints.iter().map(|&(t, k)| { - let t = ShiftBound::on(state, t, size.0); let t = SubstituteUnification::on(&substitutions, state, t); - let k = ShiftBound::on(state, k, size.0); let k = SubstituteUnification::on(&substitutions, state, k); (t, k) }); @@ -488,6 +477,11 @@ fn collect_unification(state: &mut CheckState, id: TypeId) -> UniGraph { mod tests { use super::*; + fn test_file_id() -> files::FileId { + let mut files = files::Files::default(); + files.insert("Test.purs", "module Test where\n\n") + } + fn add_unification(state: &mut CheckState, depth: u32) -> u32 { let kind = state.storage.intern(Type::Unknown); state.unification.fresh(debruijn::Size(depth), kind) @@ -495,7 +489,7 @@ mod tests { #[test] fn test_toposort_dag() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); @@ -515,7 +509,7 @@ mod tests { #[test] fn test_toposort_tuple_cycle() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); @@ -532,7 +526,7 @@ mod tests { #[test] fn test_toposort_self_cycle() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); @@ -546,7 +540,7 @@ mod tests { #[test] fn test_toposort_triple_cycle() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); @@ -564,7 +558,7 @@ mod tests { #[test] fn test_toposort_domain_ordering() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 1); @@ -582,7 +576,7 @@ mod tests { #[test] fn test_toposort_id_ordering() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); @@ -600,7 +594,7 @@ mod tests { #[test] fn test_toposort_dependency_ordering() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 2); @@ -619,7 +613,7 @@ mod tests { #[test] fn test_toposort_diamond() { - let mut state = CheckState::default(); + let mut state = CheckState::new(test_file_id()); let mut graph = UniGraph::default(); let id0 = add_unification(&mut state, 0); diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 8eca5adb..b60a4a70 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -16,7 +16,7 @@ use lowering::{ }; use resolving::ResolvedModule; use rustc_hash::FxHashMap; -use smol_str::ToSmolStr; +use smol_str::{SmolStr, ToSmolStr}; use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; @@ -24,10 +24,22 @@ use crate::algorithm::exhaustiveness::{ ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, }; use crate::algorithm::{constraint, transfer}; -use crate::core::{Type, TypeId, TypeInterner, Variable, debruijn, pretty}; +use crate::core::{Name, Type, TypeId, TypeInterner, Variable, debruijn, pretty}; use crate::error::{CheckError, ErrorKind, ErrorStep}; use crate::{CheckedModule, ExternalQueries, TypeErrorMessageId}; +/// Produces globally unique [`Name`] values. +pub struct Names { + next: u32, + file: FileId, +} + +impl Names { + pub fn new(file: FileId) -> Names { + Names { next: 0, file } + } +} + #[derive(Copy, Clone, Debug)] pub struct OperatorBranchTypes { pub left: TypeId, @@ -40,27 +52,33 @@ pub struct OperatorBranchTypes { pub struct TypeScope { pub bound: debruijn::Bound, pub kinds: debruijn::BoundMap, + pub names: FxHashMap, pub operator_node: FxHashMap, } impl TypeScope { - pub fn bind_forall(&mut self, id: TypeVariableBindingId, kind: TypeId) -> debruijn::Level { + pub fn bind_forall(&mut self, id: TypeVariableBindingId, kind: TypeId, name: Name) -> Name { let variable = debruijn::Variable::Forall(id); let level = self.bound.bind(variable); self.kinds.insert(level, kind); - level + let result = name.clone(); + self.names.insert(level, name); + result } - pub fn bind_core(&mut self, kind: TypeId) -> debruijn::Level { + pub fn bind_core(&mut self, kind: TypeId, name: Name) -> Name { let variable = debruijn::Variable::Core; let level = self.bound.bind(variable); self.kinds.insert(level, kind); - level + let result = name.clone(); + self.names.insert(level, name); + result } - pub fn lookup_forall(&self, id: TypeVariableBindingId) -> Option { + pub fn lookup_forall(&self, id: TypeVariableBindingId) -> Option { let variable = debruijn::Variable::Forall(id); - self.bound.level_of(variable) + let level = self.bound.level_of(variable)?; + self.names.get(&level).cloned() } pub fn lookup_forall_kind(&self, id: TypeVariableBindingId) -> Option { @@ -74,20 +92,20 @@ impl TypeScope { node: GraphNodeId, id: ImplicitBindingId, kind: TypeId, - ) -> debruijn::Level { + name: Name, + ) -> Name { let variable = debruijn::Variable::Implicit { node, id }; let level = self.bound.level_of(variable).unwrap_or_else(|| self.bound.bind(variable)); self.kinds.insert(level, kind); - level + let result = name.clone(); + self.names.insert(level, name); + result } - pub fn lookup_implicit( - &self, - node: GraphNodeId, - id: ImplicitBindingId, - ) -> Option { + pub fn lookup_implicit(&self, node: GraphNodeId, id: ImplicitBindingId) -> Option { let variable = debruijn::Variable::Implicit { node, id }; - self.bound.level_of(variable) + let level = self.bound.level_of(variable)?; + self.names.get(&level).cloned() } pub fn lookup_implicit_kind(&self, node: GraphNodeId, id: ImplicitBindingId) -> Option { @@ -99,6 +117,15 @@ impl TypeScope { pub fn unbind(&mut self, level: debruijn::Level) { self.bound.unbind(level); self.kinds.unbind(level); + self.names.retain(|&l, _| l < level); + } + + /// Finds the level for a given [`Name`] and unbinds from that level. + pub fn unbind_name(&mut self, name: &Name) { + let level = self.names.iter().find_map(|(&level, n)| (n == name).then_some(level)); + if let Some(level) = level { + self.unbind(level); + } } /// Unbinds variables starting from a level and returns captured implicit bindings. @@ -112,8 +139,9 @@ impl TypeScope { for (level, variable) in self.bound.iter_from(level) { if let debruijn::Variable::Implicit { node, id } = variable && let Some(&kind) = self.kinds.get(level) + && let Some(name) = self.names.get(&level).cloned() { - implicits.push(InstanceHeadBinding { node, id, kind }); + implicits.push(InstanceHeadBinding { node, id, kind, name }); } } @@ -195,6 +223,7 @@ pub struct InstanceHeadBinding { pub node: GraphNodeId, pub id: ImplicitBindingId, pub kind: TypeId, + pub name: Name, } /// Tracks type variables declared in surface syntax. @@ -268,7 +297,6 @@ impl SurfaceBindings { /// The core state structure threaded through the [`algorithm`]. /// /// [`algorithm`]: crate::algorithm -#[derive(Default)] pub struct CheckState { /// Interns and stores all types created during checking. pub storage: TypeInterner, @@ -299,6 +327,28 @@ pub struct CheckState { /// Interns patterns for exhaustiveness checking. pub patterns: PatternStorage, + + /// Produces fresh [`Name`] values for bound type variables. + pub names: Names, +} + +impl CheckState { + pub fn new(file_id: FileId) -> CheckState { + CheckState { + storage: Default::default(), + checked: Default::default(), + type_scope: Default::default(), + term_scope: Default::default(), + surface_bindings: Default::default(), + implications: Default::default(), + unification: Default::default(), + binding_group: Default::default(), + check_steps: Default::default(), + defer_synonym_expansion: Default::default(), + patterns: Default::default(), + names: Names::new(file_id), + } + } } #[derive(Clone)] @@ -1111,6 +1161,42 @@ impl CheckState { } } +/// Functions for creating fresh [`Name`] values. +impl CheckState { + pub fn fresh_name(&mut self, text: &SmolStr) -> Name { + let unique = self.names.next; + self.names.next += 1; + let file = self.names.file; + let text = SmolStr::clone(text); + let depth = self.type_scope.size(); + Name { unique, file, text, depth } + } + + pub fn fresh_name_str(&mut self, text: &str) -> Name { + let unique = self.names.next; + self.names.next += 1; + let file = self.names.file; + let text = SmolStr::new(text); + let depth = self.type_scope.size(); + Name { unique, file, text, depth } + } + + pub fn fresh_name_unify(&mut self, left: &SmolStr, right: &SmolStr) -> (Name, Name) { + let unique = self.names.next; + self.names.next += 1; + let file = self.names.file; + let depth = self.type_scope.size(); + ( + Name { unique, file, text: SmolStr::clone(left), depth }, + Name { unique, file, text: SmolStr::clone(right), depth }, + ) + } + + pub fn name_text<'a>(&self, name: &'a Name) -> &'a str { + name.text.as_str() + } +} + /// Functions for creating unification variables. impl CheckState { /// Creates a fresh unification variable with the provided depth and kind. @@ -1144,9 +1230,8 @@ impl CheckState { /// Creates a fresh skolem variable with the provided kind. pub fn fresh_skolem_kinded(&mut self, kind: TypeId) -> TypeId { - let domain = self.type_scope.size(); - let level = debruijn::Level(domain.0); - let skolem = Variable::Skolem(level, kind); + let name = self.fresh_name_str("_"); + let skolem = Variable::Skolem(name, kind); self.storage.intern(Type::Variable(skolem)) } } diff --git a/compiler-core/checking/src/algorithm/substitute.rs b/compiler-core/checking/src/algorithm/substitute.rs index f0853e6c..589fe6ff 100644 --- a/compiler-core/checking/src/algorithm/substitute.rs +++ b/compiler-core/checking/src/algorithm/substitute.rs @@ -2,85 +2,53 @@ use rustc_hash::FxHashMap; use crate::algorithm::fold::{FoldAction, TypeFold, fold_type}; use crate::algorithm::state::CheckState; -use crate::core::{ForallBinder, Type, TypeId, Variable, debruijn}; +use crate::core::{Name, Type, TypeId, Variable}; pub struct SubstituteBound { - target_level: debruijn::Level, - with_type: TypeId, + target: Name, + replacement: TypeId, } impl SubstituteBound { - /// Substitutes a bound variable at a specific level with a replacement type. + /// Substitutes a bound variable with a specific name with a replacement type. /// - /// Since levels are absolute positions, no scope tracking is needed, - /// we simply match on the target level directly. + /// Since names are globally unique, no scope tracking is needed. + /// We simply match on the target name directly. pub fn on( state: &mut CheckState, - target_level: debruijn::Level, - with_type: TypeId, + target: Name, + replacement: TypeId, in_type: TypeId, ) -> TypeId { - fold_type(state, in_type, &mut SubstituteBound { target_level, with_type }) + fold_type(state, in_type, &mut SubstituteBound { target, replacement }) } } impl TypeFold for SubstituteBound { - fn transform(&mut self, _state: &mut CheckState, _id: TypeId, t: &Type) -> FoldAction { - if let Type::Variable(Variable::Bound(level, _)) = t - && *level == self.target_level - { - return FoldAction::Replace(self.with_type); - } - FoldAction::Continue - } -} - -pub struct ShiftBound { - offset: u32, -} - -impl ShiftBound { - /// Shifts all bound variable levels in a type by a given offset. - /// - /// This is needed when adding new forall binders at the front of a type, - /// as existing bound variables need their levels adjusted to account for - /// the new binders. - pub fn on(state: &mut CheckState, id: TypeId, offset: u32) -> TypeId { - if offset == 0 { - return id; - } - fold_type(state, id, &mut ShiftBound { offset }) - } -} - -impl TypeFold for ShiftBound { - fn transform(&mut self, state: &mut CheckState, _id: TypeId, t: &Type) -> FoldAction { - if let Type::Variable(Variable::Bound(level, kind)) = t { - let level = debruijn::Level(level.0 + self.offset); - let kind = ShiftBound::on(state, *kind, self.offset); - FoldAction::Replace(state.storage.intern(Type::Variable(Variable::Bound(level, kind)))) - } else { - FoldAction::Continue + fn transform(&mut self, _state: &mut CheckState, id: TypeId, t: &Type) -> FoldAction { + match t { + // The forall rebinds the target name, so substitution stops. + Type::Forall(binder, _) if binder.variable == self.target => FoldAction::Replace(id), + Type::Variable(Variable::Bound(name, _)) if *name == self.target => { + FoldAction::Replace(self.replacement) + } + _ => FoldAction::Continue, } } - - fn transform_binder(&mut self, binder: &mut ForallBinder) { - binder.level = debruijn::Level(binder.level.0 + self.offset); - } } -pub type UniToLevel = FxHashMap; +pub type UniToName = FxHashMap; pub struct SubstituteUnification<'a> { - substitutions: &'a UniToLevel, + substitutions: &'a UniToName, } impl SubstituteUnification<'_> { - /// Level-based substitution over a [`Type`]. + /// Name-based substitution over a [`Type`]. /// - /// Replaces unification variables with bound variables using a level-based - /// mapping. Since levels are absolute positions, no scope tracking is needed. - pub fn on(substitutions: &UniToLevel, state: &mut CheckState, id: TypeId) -> TypeId { + /// Replaces unification variables with bound variables using a name-based + /// mapping. Since names are globally unique, no scope tracking is needed. + pub fn on(substitutions: &UniToName, state: &mut CheckState, id: TypeId) -> TypeId { fold_type(state, id, &mut SubstituteUnification { substitutions }) } } @@ -88,10 +56,11 @@ impl SubstituteUnification<'_> { impl TypeFold for SubstituteUnification<'_> { fn transform(&mut self, state: &mut CheckState, id: TypeId, t: &Type) -> FoldAction { if let Type::Unification(unification_id) = t { - if let Some(&(level, kind)) = self.substitutions.get(unification_id) { + if let Some((name, kind)) = self.substitutions.get(unification_id) { + let (name, kind) = (name.clone(), *kind); let kind = SubstituteUnification::on(self.substitutions, state, kind); return FoldAction::Replace( - state.storage.intern(Type::Variable(Variable::Bound(level, kind))), + state.storage.intern(Type::Variable(Variable::Bound(name, kind))), ); } return FoldAction::Replace(id); @@ -100,20 +69,20 @@ impl TypeFold for SubstituteUnification<'_> { } } -pub type LevelToType = FxHashMap; +pub type NameToType = FxHashMap; pub struct SubstituteBindings<'a> { - bindings: &'a LevelToType, + bindings: &'a NameToType, } impl SubstituteBindings<'_> { - /// Substitutes bound and implicit variables using a level-based mapping. + /// Substitutes bound variables using a name-based mapping. /// /// This is used to specialise class superclasses with instance arguments. /// For example, when deriving `Traversable (Compose f g)`, the superclass - /// `Functor t` becomes `Functor (Compose f g)` by binding `t`'s level to + /// `Functor t` becomes `Functor (Compose f g)` by binding `t`'s name to /// `Compose f g`. - pub fn on(state: &mut CheckState, bindings: &LevelToType, id: TypeId) -> TypeId { + pub fn on(state: &mut CheckState, bindings: &NameToType, id: TypeId) -> TypeId { fold_type(state, id, &mut SubstituteBindings { bindings }) } } @@ -121,8 +90,8 @@ impl SubstituteBindings<'_> { impl TypeFold for SubstituteBindings<'_> { fn transform(&mut self, _state: &mut CheckState, id: TypeId, t: &Type) -> FoldAction { match t { - Type::Variable(Variable::Bound(level, _)) => { - let id = self.bindings.get(level).copied().unwrap_or(id); + Type::Variable(Variable::Bound(name, _)) => { + let id = self.bindings.get(name).copied().unwrap_or(id); FoldAction::Replace(id) } _ => FoldAction::Continue, diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 8006406b..05841715 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1517,12 +1517,12 @@ where let function_t = state.normalize_type(function_t); match state.storage[function_t] { Type::Forall(ref binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let (argument_type, _) = kind::check_surface_kind(state, context, argument, binder_kind)?; - Ok(substitute::SubstituteBound::on(state, binder_level, argument_type, inner)) + Ok(substitute::SubstituteBound::on(state, binder_variable, argument_type, inner)) } _ => Ok(context.prim.unknown), @@ -1593,12 +1593,12 @@ where // Instantiation rule, like `toolkit::instantiate_forall` Type::Forall(ref binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let unification = state.fresh_unification_kinded(binder_kind); let function_t = - substitute::SubstituteBound::on(state, binder_level, unification, inner); + substitute::SubstituteBound::on(state, binder_variable, unification, inner); check_function_application_core(state, context, function_t, argument_id, check_argument) } @@ -1784,7 +1784,7 @@ where equation::patterns(state, context, origin, &name.equations)?; if let Some(variable) = signature.variables.first() { - state.type_scope.unbind(variable.level); + state.type_scope.unbind_name(&variable.variable); } } else { equation::infer_equations_core(state, context, name_type, &name.equations)?; diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index a9fb8020..f5974cde 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -13,7 +13,7 @@ use crate::algorithm::{ constraint, equation, inspect, kind, normalise, quantify, substitute, term, transfer, unification, }; -use crate::core::{Instance, InstanceKind, Type, TypeId, Variable, debruijn}; +use crate::core::{Instance, InstanceKind, Name, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; #[derive(Clone)] @@ -193,8 +193,8 @@ where instance.constraints = constraints.collect(); - let kind_variables = instance.kind_variables.iter().map(|&k| { - transfer::globalize(state, context, k) + let kind_variables = instance.kind_variables.iter().map(|(name, k)| { + (name.clone(), transfer::globalize(state, context, *k)) }); instance.kind_variables = kind_variables.collect(); @@ -234,11 +234,12 @@ where current = normalise::normalise_expand_type(state, context, current)?; match state.storage[current] { Type::Forall(ref binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let replacement = state.fresh_unification_kinded(binder_kind); - current = substitute::SubstituteBound::on(state, binder_level, replacement, inner); + current = + substitute::SubstituteBound::on(state, binder_variable, replacement, inner); } Type::Function(argument_kind, result_kind) => { @@ -354,7 +355,7 @@ pub struct CheckInstanceMembers<'a> { pub class_id: TypeItemId, pub instance_arguments: &'a [(TypeId, TypeId)], pub instance_constraints: &'a [(TypeId, TypeId)], - pub kind_variables: &'a [TypeId], + pub kind_variables: &'a [(Name, TypeId)], } /// Checks instance member declarations. @@ -439,7 +440,7 @@ pub struct CheckInstanceMemberGroup<'a> { class_id: TypeItemId, instance_arguments: &'a [(TypeId, TypeId)], instance_constraints: &'a [(TypeId, TypeId)], - kind_variables: &'a [TypeId], + kind_variables: &'a [(Name, TypeId)], } /// Checks an instance member group against its specialised class member type. @@ -493,13 +494,19 @@ where let size = state.type_scope.size(); // Bind kind variables generalised after instance head checking. - for &kind_variable in kind_variables { - let kind = transfer::localize(state, context, kind_variable); - state.type_scope.bind_core(kind); + for (name, kind_variable) in kind_variables { + let kind = transfer::localize(state, context, *kind_variable); + let name = state.fresh_name(&name.text); + state.type_scope.bind_core(kind, name); } for binding in instance_bindings { - state.type_scope.bind_implicit(binding.node, binding.id, binding.kind); + state.type_scope.bind_implicit( + binding.node, + binding.id, + binding.kind, + binding.name.clone(), + ); } let class_member_type = lookup_class_member(state, context, member.resolution)?; @@ -639,8 +646,7 @@ where // instance Functor (Vector s:0 n:1) where // -- map :: forall f:2 a:3 b:4. (a -> b) -> f a -> f b // ``` - let debruijn::Size(scope_size) = state.type_scope.size(); - specialised = substitute::ShiftBound::on(state, specialised, scope_size); + // With globally unique Names, shifting is no longer needed. let mut kind_variables = 0..kind_variables; let mut arguments = arguments.into_iter(); @@ -648,7 +654,7 @@ where while let normalized = state.normalize_type(specialised) && let Type::Forall(binder, inner) = &state.storage[normalized] { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let inner = *inner; @@ -658,11 +664,11 @@ where let _ = unification::unify(state, context, binder_kind, argument_kind); argument_type } else { - let skolem = Variable::Skolem(binder_level, binder_kind); + let skolem = Variable::Skolem(binder_variable.clone(), binder_kind); state.storage.intern(Type::Variable(skolem)) }; - specialised = substitute::SubstituteBound::on(state, binder_level, replacement, inner); + specialised = substitute::SubstituteBound::on(state, binder_variable, replacement, inner); } specialised = state.normalize_type(specialised); diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 301adff1..9944f63e 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -101,11 +101,11 @@ pub fn instantiate_forall(state: &mut CheckState, mut type_id: TypeId) -> TypeId safe_loop! { type_id = state.normalize_type(type_id); if let Type::Forall(ref binder, inner) = state.storage[type_id] { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let unification = state.fresh_unification_kinded(binder_kind); - type_id = substitute::SubstituteBound::on(state, binder_level, unification, inner); + type_id = substitute::SubstituteBound::on(state, binder_variable, unification, inner); } else { break type_id; } @@ -121,12 +121,12 @@ pub fn skolemise_forall(state: &mut CheckState, mut type_id: TypeId) -> TypeId { safe_loop! { type_id = state.normalize_type(type_id); if let Type::Forall(ref binder, inner) = state.storage[type_id] { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; - let v = Variable::Skolem(binder_level, binder_kind); + let v = Variable::Skolem(binder_variable.clone(), binder_kind); let t = state.storage.intern(Type::Variable(v)); - type_id = substitute::SubstituteBound::on(state, binder_level, t, inner); + type_id = substitute::SubstituteBound::on(state, binder_variable, t, inner); } else { break type_id; } @@ -197,18 +197,18 @@ pub fn instantiate_with_arguments( type_id = state.normalize_type(type_id); match &state.storage[type_id] { Type::Forall(binder, inner) => { - let binder_level = binder.level; + let binder_variable = binder.variable.clone(); let binder_kind = binder.kind; let inner = *inner; let argument_type = arguments_iter.next().unwrap_or_else(|| { skolemised += 1; - let skolem = Variable::Skolem(binder_level, binder_kind); + let skolem = Variable::Skolem(binder_variable.clone(), binder_kind); state.storage.intern(Type::Variable(skolem)) }); type_id = - substitute::SubstituteBound::on(state, binder_level, argument_type, inner); + substitute::SubstituteBound::on(state, binder_variable, argument_type, inner); } _ => break, } diff --git a/compiler-core/checking/src/algorithm/transfer.rs b/compiler-core/checking/src/algorithm/transfer.rs index 69894497..7f68d6ca 100644 --- a/compiler-core/checking/src/algorithm/transfer.rs +++ b/compiler-core/checking/src/algorithm/transfer.rs @@ -164,9 +164,9 @@ fn traverse<'a, Q: ExternalQueries>(source: &mut TraversalSource<'a, Q>, id: Typ let kind = traverse(source, kind); Type::Variable(Variable::Skolem(level, kind)) } - Variable::Bound(level, kind) => { + Variable::Bound(name, kind) => { let kind = traverse(source, kind); - Type::Variable(Variable::Bound(level, kind)) + Type::Variable(Variable::Bound(name, kind)) } free @ Variable::Free(_) => Type::Variable(free), }, diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 6ffbd9c3..81afa86c 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -141,8 +141,13 @@ fn check_data_definition( where Q: ExternalQueries, { - let Some(SignatureLike { kind_variables, type_variables, result_kind }) = - check_signature_like(state, context, item_id, signature, variables, |_| context.prim.t)? + let Some(SignatureLike { + kind_variables, + type_variables, + result_kind, + kind_unbind_level, + type_unbind_level, + }) = check_signature_like(state, context, item_id, signature, variables, |_| context.prim.t)? else { return Ok(None); }; @@ -160,9 +165,6 @@ where let constructors = check_constructor_arguments(state, context, item_id)?; - let type_unbind_level = type_variables.first().map(|variable| variable.level); - let kind_unbind_level = kind_variables.first().map(|variable| variable.level); - if let Some(level) = type_unbind_level { state.type_scope.unbind(level); } @@ -193,10 +195,15 @@ fn check_synonym_definition( where Q: ExternalQueries, { - let Some(SignatureLike { kind_variables, type_variables, result_kind }) = - check_signature_like(state, context, item_id, signature, variables, |state| { - state.fresh_unification_type(context) - })? + let Some(SignatureLike { + kind_variables, + type_variables, + result_kind, + kind_unbind_level, + type_unbind_level, + }) = check_signature_like(state, context, item_id, signature, variables, |state| { + state.fresh_unification_type(context) + })? else { return Ok(None); }; @@ -213,11 +220,11 @@ where inferred_kind.replace(synonym_kind); } - if let Some(variable) = type_variables.first() { - state.type_scope.unbind(variable.level); + if let Some(level) = type_unbind_level { + state.type_scope.unbind(level); } - if let Some(variable) = kind_variables.first() { - state.type_scope.unbind(variable.level); + if let Some(level) = kind_unbind_level { + state.type_scope.unbind(level); } crate::debug_fields!(state, context, { synonym_type = synonym_type }); @@ -241,10 +248,15 @@ fn check_class_definition( where Q: ExternalQueries, { - let Some(SignatureLike { kind_variables, type_variables, result_kind }) = - check_signature_like(state, context, item_id, signature, variables, |_| { - context.prim.constraint - })? + let Some(SignatureLike { + kind_variables, + type_variables, + result_kind, + kind_unbind_level, + type_unbind_level, + }) = check_signature_like(state, context, item_id, signature, variables, |_| { + context.prim.constraint + })? else { return Ok(None); }; @@ -261,7 +273,7 @@ where let class_reference = { let reference_type = state.storage.intern(Type::Constructor(context.id, item_id)); type_variables.iter().cloned().fold(reference_type, |reference_type, binder| { - let variable = Variable::Bound(binder.level, binder.kind); + let variable = Variable::Bound(binder.variable, binder.kind); let variable = state.storage.intern(Type::Variable(variable)); state.storage.intern(Type::Application(reference_type, variable)) }) @@ -286,11 +298,11 @@ where class_reference, )?; - if let Some(variable) = type_variables.first() { - state.type_scope.unbind(variable.level); + if let Some(level) = type_unbind_level { + state.type_scope.unbind(level); } - if let Some(variable) = kind_variables.first() { - state.type_scope.unbind(variable.level); + if let Some(level) = kind_unbind_level { + state.type_scope.unbind(level); } crate::debug_fields!(state, context, { ?superclass_count = superclasses.len(), ?member_count = members.len() }); @@ -500,7 +512,7 @@ where let reference_type = state.storage.intern(Type::Constructor(context.id, item_id)); let reference_type = kind_variables.iter().fold(reference_type, |reference, binder| { - let variable = Variable::Bound(binder.level, binder.kind); + let variable = Variable::Bound(binder.variable.clone(), binder.kind); let variable = state.storage.intern(Type::Variable(variable)); state.storage.intern(Type::KindApplication(reference, variable)) }); @@ -518,7 +530,7 @@ where }); type_variables.iter().fold(reference_type, |reference, binder| { - let variable = Variable::Bound(binder.level, binder.kind); + let variable = Variable::Bound(binder.variable.clone(), binder.kind); let variable = state.storage.intern(Type::Variable(variable)); state.storage.intern(Type::Application(reference, variable)) }) @@ -537,31 +549,31 @@ where let CheckedClass { inferred_kind, kind_variables, type_variables, superclasses, members } = checked; - let mut quantified_type = None; - let mut quantified_variables = debruijn::Size(0); - if let Some(inferred_kind) = inferred_kind - && let Some((q_type, q_variables)) = quantify::quantify(state, inferred_kind) + && let Some((quantified_type, _)) = quantify::quantify(state, inferred_kind) { - quantified_type = Some(q_type); - quantified_variables = q_variables; - }; + let type_id = transfer::globalize(state, context, quantified_type); + state.checked.types.insert(item_id, type_id); + } let mut class = { let kind_var_count = kind_variables.len() as u32; let kind_variables = debruijn::Size(kind_var_count); let type_variable_kinds = type_variables.iter().map(|binder| binder.kind).collect(); - Class { superclasses, type_variable_kinds, quantified_variables, kind_variables } + let type_variable_names = + type_variables.iter().map(|binder| binder.variable.clone()).collect(); + Class { + superclasses, + type_variable_kinds, + type_variable_names, + quantified_variables: debruijn::Size(0), + kind_variables, + } }; - let class_quantified_count = + let quantified_variables = quantify::quantify_class(state, &mut class).unwrap_or(debruijn::Size(0)); - debug_assert_eq!( - quantified_variables, class_quantified_count, - "critical violation: class type signature and declaration should have the same number of variables" - ); - class.quantified_variables = quantified_variables; let superclasses = class.superclasses.iter().map(|&(t, k)| { @@ -579,11 +591,6 @@ where state.checked.classes.insert(item_id, class); - if let Some(quantified_type) = quantified_type { - let type_id = transfer::globalize(state, context, quantified_type); - state.checked.types.insert(item_id, type_id); - } - for (member_id, member_type) in members { if let Some((quantified_member, _)) = quantify::quantify(state, member_type) { let member_type = transfer::globalize(state, context, quantified_member); @@ -678,6 +685,12 @@ struct SignatureLike { kind_variables: Vec, type_variables: Vec, result_kind: TypeId, + /// The scope level before kind_variables were bound, + /// used for unbinding. + kind_unbind_level: Option, + /// The scope level before type_variables were bound, + /// used for unbinding. + type_unbind_level: Option, } fn check_signature_like( @@ -697,8 +710,14 @@ where let surface_bindings = state.surface_bindings.get_type(item_id); let surface_bindings = surface_bindings.as_deref().unwrap_or_default(); + // Capture scope level before inspect_signature binds kind variables. + let kind_unbind_level = debruijn::Level(state.type_scope.size().0); + let signature = inspect::inspect_signature(state, context, stored_kind, surface_bindings)?; + let kind_unbind_level = + if signature.variables.is_empty() { None } else { Some(kind_unbind_level) }; + // The kind signature may have more function arrows than the // definition has parameters when the result kind is itself a // function kind. For example: @@ -715,8 +734,8 @@ where actual: 0, }); - if let Some(variable) = signature.variables.first() { - state.type_scope.unbind(variable.level); + if let Some(level) = kind_unbind_level { + state.type_scope.unbind(level); } return Ok(None); @@ -761,17 +780,35 @@ where let result_kind = excess_arguments.iter().rfold(signature.result, |result, &argument| { state.storage.intern(Type::Function(argument, result)) }); - let type_variables = kinds.into_iter().map(|(id, visible, name, kind)| { - let level = state.type_scope.bind_forall(id, kind); - ForallBinder { visible, implicit: false, name, level, kind } + + // Capture scope level before binding type variables. + let type_unbind_level = debruijn::Level(state.type_scope.size().0); + + let type_variables = kinds.into_iter().map(|(id, visible, text, kind)| { + let name = state.fresh_name(&text); + state.type_scope.bind_forall(id, kind, name.clone()); + ForallBinder { visible, implicit: false, text, variable: name, kind } }); let type_variables = type_variables.collect_vec(); - SignatureLike { kind_variables, type_variables, result_kind } + let type_unbind_level = + if type_variables.is_empty() { None } else { Some(type_unbind_level) }; + + SignatureLike { + kind_variables, + type_variables, + result_kind, + kind_unbind_level, + type_unbind_level, + } } else { let kind_variables = vec![]; let result_kind = infer_result(state); + + // Capture scope level before binding type variables. + let type_unbind_level = debruijn::Level(state.type_scope.size().0); + let type_variables = variables.iter().map(|variable| { let kind = if let Some(id) = variable.kind { let (kind, _) = kind::check_surface_kind(state, context, id, context.prim.t)?; @@ -781,14 +818,24 @@ where }; let visible = variable.visible; - let name = variable.name.clone().unwrap_or(MISSING_NAME); - let level = state.type_scope.bind_forall(variable.id, kind); - Ok(ForallBinder { visible, implicit: false, name, level, kind }) + let text = variable.name.clone().unwrap_or(MISSING_NAME); + let name = state.fresh_name(&text); + state.type_scope.bind_forall(variable.id, kind, name.clone()); + Ok(ForallBinder { visible, implicit: false, text, variable: name, kind }) }); let type_variables = type_variables.collect::>>()?; - SignatureLike { kind_variables, type_variables, result_kind } + let type_unbind_level = + if type_variables.is_empty() { None } else { Some(type_unbind_level) }; + + SignatureLike { + kind_variables, + type_variables, + result_kind, + kind_unbind_level: None, + type_unbind_level, + } }; Ok(Some(signature)) @@ -908,8 +955,8 @@ fn infer_roles( ) { let type_id = state.normalize_type(type_id); match state.storage[type_id].clone() { - Type::Variable(Variable::Bound(level, _)) => { - if let Some(index) = variables.iter().position(|v| v.level == level) { + Type::Variable(Variable::Bound(name, _)) => { + if let Some(index) = variables.iter().position(|v| v.variable == name) { // The following cases infer to nominal roles: // // ``` diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 34b95a64..e77f6ace 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -120,10 +120,10 @@ where } (_, Type::Forall(ref binder, inner)) => { - let v = Variable::Skolem(binder.level, binder.kind); + let v = Variable::Skolem(binder.variable.clone(), binder.kind); let t = state.storage.intern(Type::Variable(v)); - let inner = substitute::SubstituteBound::on(state, binder.level, t, inner); + let inner = substitute::SubstituteBound::on(state, binder.variable.clone(), t, inner); subtype_with_mode(state, context, t1, inner, mode) } @@ -131,7 +131,7 @@ where let k = state.normalize_type(binder.kind); let t = state.fresh_unification_kinded(k); - let inner = substitute::SubstituteBound::on(state, binder.level, t, inner); + let inner = substitute::SubstituteBound::on(state, binder.variable.clone(), t, inner); subtype_with_mode(state, context, inner, t2, mode) } @@ -209,16 +209,25 @@ where } (Type::Forall(t1_binder, t1_inner), Type::Forall(t2_binder, t2_inner)) => { - let t1 = { - let v = Variable::Skolem(t1_binder.level, t1_binder.kind); - let t = state.storage.intern(Type::Variable(v)); - substitute::SubstituteBound::on(state, t1_binder.level, t, t1_inner) + unify(state, context, t1_binder.kind, t2_binder.kind)?; + + let (t1_name, t2_name) = state.fresh_name_unify(&t1_binder.text, &t2_binder.text); + + let t1_skolem = { + let skolem = Variable::Skolem(t1_name, t1_binder.kind); + state.storage.intern(Type::Variable(skolem)) }; - let t2 = { - let v = Variable::Skolem(t2_binder.level, t2_binder.kind); - let t = state.storage.intern(Type::Variable(v)); - substitute::SubstituteBound::on(state, t2_binder.level, t, t2_inner) + + let t2_skolem = { + let skolem = Variable::Skolem(t2_name, t2_binder.kind); + state.storage.intern(Type::Variable(skolem)) }; + + let t1 = + substitute::SubstituteBound::on(state, t1_binder.variable, t1_skolem, t1_inner); + let t2 = + substitute::SubstituteBound::on(state, t2_binder.variable, t2_skolem, t2_inner); + unify(state, context, t1, t2)? } @@ -256,10 +265,10 @@ where (Type::Row(t1_row), Type::Row(t2_row)) => unify_rows(state, context, t1_row, t2_row)?, ( - Type::Variable(Variable::Bound(t1_level, t1_kind)), - Type::Variable(Variable::Bound(t2_level, t2_kind)), + Type::Variable(Variable::Bound(t1_name, t1_kind)), + Type::Variable(Variable::Bound(t2_name, t2_kind)), ) => { - if t1_level == t2_level { + if t1_name == t2_name { unify(state, context, t1_kind, t2_kind)? } else { false @@ -467,17 +476,18 @@ pub fn promote_type( // Without the third rule, `c` at level 2 >= C(1) would be // rejected as escaping, breaking `?a := forall c. Maybe c`. match variable { - Variable::Bound(level, kind) => { - if level.0 >= c.solve_depth.0 { - // S <= level + Variable::Bound(name, kind) => { + let bound_depth = name.depth; + if bound_depth.0 >= c.solve_depth.0 { + // S <= depth return aux(s, c, depth, *kind); } let unification = s.unification.get(c.unification_id); - if level.0 >= unification.depth.0 { - // C <= level < S + if bound_depth.0 >= unification.depth.0 { + // C <= depth < S return false; } - // level < C + // depth < C aux(s, c, depth, *kind) } Variable::Skolem(_, kind) => aux(s, c, depth, *kind), diff --git a/compiler-core/checking/src/core.rs b/compiler-core/checking/src/core.rs index b43c1d32..0282b574 100644 --- a/compiler-core/checking/src/core.rs +++ b/compiler-core/checking/src/core.rs @@ -3,25 +3,63 @@ pub mod debruijn; pub mod pretty; +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use files::FileId; use indexing::{InstanceChainId, TypeItemId}; use smol_str::SmolStr; +/// Globally unique identity for a bound type variable. +#[derive(Debug, Clone)] +pub struct Name { + pub unique: u32, + pub file: FileId, + pub text: SmolStr, + pub depth: debruijn::Size, +} + +impl PartialEq for Name { + fn eq(&self, other: &Name) -> bool { + self.file == other.file && self.unique == other.unique + } +} + +impl Eq for Name {} + +impl Hash for Name { + fn hash(&self, state: &mut H) { + self.file.hash(state); + self.unique.hash(state); + } +} + +impl PartialOrd for Name { + fn partial_cmp(&self, other: &Name) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Name { + fn cmp(&self, other: &Name) -> Ordering { + self.file.cmp(&other.file).then(self.unique.cmp(&other.unique)) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ForallBinder { pub visible: bool, pub implicit: bool, - pub name: SmolStr, - pub level: debruijn::Level, + pub text: SmolStr, + pub variable: Name, pub kind: TypeId, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Variable { - Skolem(debruijn::Level, TypeId), - Bound(debruijn::Level, TypeId), + Skolem(Name, TypeId), + Bound(Name, TypeId), Free(SmolStr), } @@ -125,13 +163,14 @@ pub struct Instance { pub constraints: Vec<(TypeId, TypeId)>, pub resolution: (FileId, TypeItemId), pub kind: InstanceKind, - pub kind_variables: Vec, + pub kind_variables: Vec<(Name, TypeId)>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Class { pub superclasses: Arc<[(TypeId, TypeId)]>, pub type_variable_kinds: Vec, + pub type_variable_names: Vec, pub quantified_variables: debruijn::Size, pub kind_variables: debruijn::Size, } diff --git a/compiler-core/checking/src/core/pretty.rs b/compiler-core/checking/src/core/pretty.rs index 2bddcfa5..5b435b4f 100644 --- a/compiler-core/checking/src/core/pretty.rs +++ b/compiler-core/checking/src/core/pretty.rs @@ -295,16 +295,16 @@ where let binder_docs = binders .iter() - .map(|ForallBinder { name, kind, .. }| { + .map(|ForallBinder { text, variable, kind, .. }| { let kind_doc = traverse_precedence(arena, source, context, Precedence::Top, *kind); - context.names.insert(context.depth.0, name.to_string()); + context.names.insert(variable.unique, text.to_string()); context.depth = debruijn::Size(context.depth.0 + 1); // Group each binder so it stays together as an atomic unit arena .text("(") - .append(arena.text(name.to_string())) + .append(arena.text(text.to_string())) .append(arena.text(" :: ")) .append(kind_doc) .append(arena.text(")")) @@ -482,22 +482,22 @@ where Q: ExternalQueries, { match variable { - Variable::Skolem(level, kind) => { + Variable::Skolem(name, kind) => { + let name = format!("~{}", name.text); let kind_doc = traverse_precedence(arena, source, context, Precedence::Top, *kind); arena .text("(") - .append(arena.text(format!("~{}", level))) + .append(arena.text(name)) .append(arena.text(" :: ")) .append(kind_doc) .append(arena.text(")")) } - Variable::Bound(level, kind) => { - let name = context.names.get(&level.0).cloned(); - let name_doc = arena.text(name.unwrap_or_else(|| format!("{}", level))); + Variable::Bound(name, kind) => { + let name = name.text.to_string(); let kind_doc = traverse_precedence(arena, source, context, Precedence::Top, *kind); arena .text("(") - .append(name_doc) + .append(arena.text(name)) .append(arena.text(" :: ")) .append(kind_doc) .append(arena.text(")")) diff --git a/tests-integration/fixtures/checking/010_class_basic/Main.snap b/tests-integration/fixtures/checking/010_class_basic/Main.snap index 108405ff..a3a853a1 100644 --- a/tests-integration/fixtures/checking/010_class_basic/Main.snap +++ b/tests-integration/fixtures/checking/010_class_basic/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,4 +10,4 @@ Types Show :: Type -> Constraint Classes -class Show (&0 :: Type) +class Show (a :: Type) diff --git a/tests-integration/fixtures/checking/011_class_functor/Main.snap b/tests-integration/fixtures/checking/011_class_functor/Main.snap index 343f855f..452312f9 100644 --- a/tests-integration/fixtures/checking/011_class_functor/Main.snap +++ b/tests-integration/fixtures/checking/011_class_functor/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -14,4 +15,4 @@ Types Functor :: (Type -> Type) -> Constraint Classes -class Functor (&0 :: Type -> Type) +class Functor (f :: Type -> Type) diff --git a/tests-integration/fixtures/checking/012_class_monad_state/Main.snap b/tests-integration/fixtures/checking/012_class_monad_state/Main.snap index 896f426f..d09dc259 100644 --- a/tests-integration/fixtures/checking/012_class_monad_state/Main.snap +++ b/tests-integration/fixtures/checking/012_class_monad_state/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -19,5 +20,5 @@ Monad :: (Type -> Type) -> Constraint MonadState :: Type -> (Type -> Type) -> Constraint Classes -class Monad (&0 :: Type -> Type) -class Monad (&1 :: Type -> Type) <= MonadState (&0 :: Type) (&1 :: Type -> Type) +class Monad (m :: Type -> Type) +class Monad (m :: Type -> Type) <= MonadState (s :: Type) (m :: Type -> Type) diff --git a/tests-integration/fixtures/checking/013_class_phantom/Main.snap b/tests-integration/fixtures/checking/013_class_phantom/Main.snap index 01403597..ffc8a79d 100644 --- a/tests-integration/fixtures/checking/013_class_phantom/Main.snap +++ b/tests-integration/fixtures/checking/013_class_phantom/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,4 +10,4 @@ Types Phantom :: forall (t0 :: Type). (t0 :: Type) -> Constraint Classes -class Phantom (&1 :: (&0 :: Type)) +class Phantom (a :: (t0 :: Type)) diff --git a/tests-integration/fixtures/checking/014_class_with_signature/Main.snap b/tests-integration/fixtures/checking/014_class_with_signature/Main.snap index 343f855f..452312f9 100644 --- a/tests-integration/fixtures/checking/014_class_with_signature/Main.snap +++ b/tests-integration/fixtures/checking/014_class_with_signature/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -14,4 +15,4 @@ Types Functor :: (Type -> Type) -> Constraint Classes -class Functor (&0 :: Type -> Type) +class Functor (f :: Type -> Type) diff --git a/tests-integration/fixtures/checking/015_class_superclass/Main.snap b/tests-integration/fixtures/checking/015_class_superclass/Main.snap index 4df74fab..ce05684a 100644 --- a/tests-integration/fixtures/checking/015_class_superclass/Main.snap +++ b/tests-integration/fixtures/checking/015_class_superclass/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,5 +19,5 @@ Functor :: (Type -> Type) -> Constraint Applicative :: (Type -> Type) -> Constraint Classes -class Functor (&0 :: Type -> Type) -class Functor (&0 :: Type -> Type) <= Applicative (&0 :: Type -> Type) +class Functor (f :: Type -> Type) +class Functor (f :: Type -> Type) <= Applicative (f :: Type -> Type) diff --git a/tests-integration/fixtures/checking/027_type_constrained/Main.snap b/tests-integration/fixtures/checking/027_type_constrained/Main.snap index 0354e07f..1ab5f67a 100644 --- a/tests-integration/fixtures/checking/027_type_constrained/Main.snap +++ b/tests-integration/fixtures/checking/027_type_constrained/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -17,4 +18,4 @@ Showable = Show Int => Int Classes -class Show (&0 :: Type) +class Show (a :: Type) diff --git a/tests-integration/fixtures/checking/076_inspect_constraints/Main.snap b/tests-integration/fixtures/checking/076_inspect_constraints/Main.snap index d21b8bf8..17dec384 100644 --- a/tests-integration/fixtures/checking/076_inspect_constraints/Main.snap +++ b/tests-integration/fixtures/checking/076_inspect_constraints/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -38,5 +39,5 @@ NestedConstraint = forall (a :: Type) (b :: Type). Show (b :: Type) => (a :: Typ Classes -class Show (&0 :: Type) -class Eq (&0 :: Type) +class Show (a :: Type) +class Eq (a :: Type) diff --git a/tests-integration/fixtures/checking/083_instance_basic/Main.snap b/tests-integration/fixtures/checking/083_instance_basic/Main.snap index fe16548c..3a5720a4 100644 --- a/tests-integration/fixtures/checking/083_instance_basic/Main.snap +++ b/tests-integration/fixtures/checking/083_instance_basic/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,8 +10,8 @@ Types Eq :: Type -> Constraint Classes -class Eq (&0 :: Type) +class Eq (a :: Type) Instances -instance Eq (&0 :: Type) => Eq (Array (&0 :: Type) :: Type) +instance Eq (a :: Type) => Eq (Array (a :: Type) :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/084_instance_eq/Main.snap b/tests-integration/fixtures/checking/084_instance_eq/Main.snap index 8da71424..3eb4d7b4 100644 --- a/tests-integration/fixtures/checking/084_instance_eq/Main.snap +++ b/tests-integration/fixtures/checking/084_instance_eq/Main.snap @@ -11,7 +11,7 @@ Types Eq :: Type -> Constraint Classes -class Eq (&0 :: Type) +class Eq (a :: Type) Instances instance Eq (Int :: Type) diff --git a/tests-integration/fixtures/checking/085_instance_functional_dependency/Main.snap b/tests-integration/fixtures/checking/085_instance_functional_dependency/Main.snap index 3a0e10e7..1db6b21a 100644 --- a/tests-integration/fixtures/checking/085_instance_functional_dependency/Main.snap +++ b/tests-integration/fixtures/checking/085_instance_functional_dependency/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -11,7 +12,7 @@ Types Convert :: Type -> Type -> Constraint Classes -class Convert (&0 :: Type) (&1 :: Type) +class Convert (a :: Type) (b :: Type) Instances instance Convert (Int :: Type) (String :: Type) diff --git a/tests-integration/fixtures/checking/086_instance_functional_dependency_transitive/Main.snap b/tests-integration/fixtures/checking/086_instance_functional_dependency_transitive/Main.snap index 29a2bbe6..495123ab 100644 --- a/tests-integration/fixtures/checking/086_instance_functional_dependency_transitive/Main.snap +++ b/tests-integration/fixtures/checking/086_instance_functional_dependency_transitive/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -12,7 +13,7 @@ Types Chain :: forall (t1 :: Type). Type -> (t1 :: Type) -> Type -> Constraint Classes -class Chain (&1 :: Type) (&2 :: (&0 :: Type)) (&3 :: Type) +class Chain (a :: Type) (b :: (t1 :: Type)) (c :: Type) Instances instance Chain (Int :: Type) (String :: Type) (Boolean :: Type) diff --git a/tests-integration/fixtures/checking/087_instance_functional_dependency_multiple/Main.snap b/tests-integration/fixtures/checking/087_instance_functional_dependency_multiple/Main.snap index af72085f..fa661789 100644 --- a/tests-integration/fixtures/checking/087_instance_functional_dependency_multiple/Main.snap +++ b/tests-integration/fixtures/checking/087_instance_functional_dependency_multiple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -12,7 +13,7 @@ Types TypeEq :: Type -> Type -> Type -> Constraint Classes -class TypeEq (&0 :: Type) (&1 :: Type) (&2 :: Type) +class TypeEq (a :: Type) (b :: Type) (c :: Type) Instances instance TypeEq (Int :: Type) (Int :: Type) (Boolean :: Type) diff --git a/tests-integration/fixtures/checking/088_given_constraint_matching/Main.snap b/tests-integration/fixtures/checking/088_given_constraint_matching/Main.snap index 58c38874..76d52f9f 100644 --- a/tests-integration/fixtures/checking/088_given_constraint_matching/Main.snap +++ b/tests-integration/fixtures/checking/088_given_constraint_matching/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -10,4 +11,4 @@ Types Eq :: Type -> Constraint Classes -class Eq (&0 :: Type) +class Eq (a :: Type) diff --git a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap index c7b8f65a..3bc5832b 100644 --- a/tests-integration/fixtures/checking/089_no_instance_found/Main.snap +++ b/tests-integration/fixtures/checking/089_no_instance_found/Main.snap @@ -22,7 +22,7 @@ Roles Foo = [] Classes -class Eq (&0 :: Type) +class Eq (a :: Type) Diagnostics error[NoInstanceFound]: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/090_instance_improve/Main.snap b/tests-integration/fixtures/checking/090_instance_improve/Main.snap index 56b86cde..daae1dd6 100644 --- a/tests-integration/fixtures/checking/090_instance_improve/Main.snap +++ b/tests-integration/fixtures/checking/090_instance_improve/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -28,8 +29,8 @@ True = [] False = [] Classes -class TypeEq (&1 :: Type) (&2 :: Type) (&3 :: (&0 :: Type)) +class TypeEq (a :: Type) (b :: Type) (r :: (t2 :: Type)) Instances -instance TypeEq ((&0 :: Type) :: Type) ((&0 :: Type) :: Type) (True :: Type) +instance TypeEq ((a :: Type) :: Type) ((a :: Type) :: Type) (True :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/091_superclass_elaboration/Main.snap b/tests-integration/fixtures/checking/091_superclass_elaboration/Main.snap index 6dea7d9b..dd192af6 100644 --- a/tests-integration/fixtures/checking/091_superclass_elaboration/Main.snap +++ b/tests-integration/fixtures/checking/091_superclass_elaboration/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -26,5 +27,5 @@ Roles Ordering = [] Classes -class Eq (&0 :: Type) -class Eq (&0 :: Type) <= Ord (&0 :: Type) +class Eq (a :: Type) +class Eq (a :: Type) <= Ord (a :: Type) diff --git a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap index bfe8a594..ba31985e 100644 --- a/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap +++ b/tests-integration/fixtures/checking/092_ambiguous_constraint/Main.snap @@ -13,8 +13,8 @@ Read :: Type -> Constraint Show :: Type -> Constraint Classes -class Read (&0 :: Type) -class Show (&0 :: Type) +class Read (a :: Type) +class Show (a :: Type) Diagnostics error[AmbiguousConstraint]: Ambiguous constraint: Show ?5[:0] diff --git a/tests-integration/fixtures/checking/093_constraint_generalization/Main.snap b/tests-integration/fixtures/checking/093_constraint_generalization/Main.snap index c898b941..2f6573d1 100644 --- a/tests-integration/fixtures/checking/093_constraint_generalization/Main.snap +++ b/tests-integration/fixtures/checking/093_constraint_generalization/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,5 +14,5 @@ Eq :: Type -> Constraint Ord :: Type -> Constraint Classes -class Eq (&0 :: Type) -class Ord (&0 :: Type) +class Eq (a :: Type) +class Ord (a :: Type) diff --git a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap index 120a0d4a..316ddb45 100644 --- a/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap +++ b/tests-integration/fixtures/checking/094_let_binding_constraint_error/Main.snap @@ -22,7 +22,7 @@ Roles Foo = [] Classes -class Eq (&0 :: Type) +class Eq (a :: Type) Diagnostics error[NoInstanceFound]: No instance found for: Eq Foo diff --git a/tests-integration/fixtures/checking/095_given_constraint_arityless/Main.snap b/tests-integration/fixtures/checking/095_given_constraint_arityless/Main.snap index babad5c7..5a92146a 100644 --- a/tests-integration/fixtures/checking/095_given_constraint_arityless/Main.snap +++ b/tests-integration/fixtures/checking/095_given_constraint_arityless/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -19,5 +20,5 @@ Eq :: Type -> Constraint Coercible :: Type -> Type -> Constraint Classes -class Eq (&0 :: Type) -class Coercible (&0 :: Type) (&1 :: Type) +class Eq (a :: Type) +class Coercible (a :: Type) (b :: Type) diff --git a/tests-integration/fixtures/checking/096_given_functional_dependency/Main.snap b/tests-integration/fixtures/checking/096_given_functional_dependency/Main.snap index a4c973e1..eac0f4fc 100644 --- a/tests-integration/fixtures/checking/096_given_functional_dependency/Main.snap +++ b/tests-integration/fixtures/checking/096_given_functional_dependency/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -16,5 +17,5 @@ Convert :: Type -> Type -> Constraint Relate :: Type -> Type -> Type -> Constraint Classes -class Convert (&0 :: Type) (&1 :: Type) -class Relate (&0 :: Type) (&1 :: Type) (&2 :: Type) +class Convert (a :: Type) (b :: Type) +class Relate (a :: Type) (b :: Type) (c :: Type) diff --git a/tests-integration/fixtures/checking/097_instance_chains/Main.snap b/tests-integration/fixtures/checking/097_instance_chains/Main.snap index 042b3c31..1a3ff3dc 100644 --- a/tests-integration/fixtures/checking/097_instance_chains/Main.snap +++ b/tests-integration/fixtures/checking/097_instance_chains/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -30,10 +31,10 @@ Roles Proxy = [Phantom] Classes -class TypeEq (&3 :: (&0 :: Type)) (&4 :: (&1 :: Type)) (&5 :: (&2 :: Type)) +class TypeEq (a :: (t1 :: Type)) (b :: (t2 :: Type)) (r :: (t3 :: Type)) Instances -instance forall (&0 :: Type). TypeEq ((&1 :: (&0 :: Type)) :: (&0 :: Type)) ((&1 :: (&0 :: Type)) :: (&0 :: Type)) (True :: Boolean) +instance forall (t19 :: Type). TypeEq ((a :: (t19 :: Type)) :: (t19 :: Type)) ((a :: (t19 :: Type)) :: (t19 :: Type)) (True :: Boolean) chain: 0 -instance forall (&0 :: Type) (&1 :: Type). TypeEq ((&2 :: (&0 :: Type)) :: (&0 :: Type)) ((&3 :: (&1 :: Type)) :: (&1 :: Type)) (False :: Boolean) +instance forall (t24 :: Type) (t23 :: Type). TypeEq ((a :: (t23 :: Type)) :: (t23 :: Type)) ((b :: (t24 :: Type)) :: (t24 :: Type)) (False :: Boolean) chain: 1 diff --git a/tests-integration/fixtures/checking/098_fundep_propagation/Main.snap b/tests-integration/fixtures/checking/098_fundep_propagation/Main.snap index ba03d54d..b6850859 100644 --- a/tests-integration/fixtures/checking/098_fundep_propagation/Main.snap +++ b/tests-integration/fixtures/checking/098_fundep_propagation/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -37,13 +38,13 @@ Z = [] S = [Nominal] Classes -class IsZero (&2 :: (&0 :: Type)) (&3 :: (&1 :: Type)) -class And (&3 :: (&0 :: Type)) (&4 :: (&1 :: Type)) (&5 :: (&2 :: Type)) +class IsZero (n :: (t1 :: Type)) (r :: (t2 :: Type)) +class And (a :: (t3 :: Type)) (b :: (t4 :: Type)) (r :: (t5 :: Type)) Instances instance IsZero (Z :: Type) (True :: Boolean) chain: 0 -instance IsZero (S (&0 :: Type) :: Type) (False :: Boolean) +instance IsZero (S (n :: Type) :: Type) (False :: Boolean) chain: 0 instance And (True :: Boolean) (True :: Boolean) (True :: Boolean) chain: 0 diff --git a/tests-integration/fixtures/checking/102_builtin_row/Main.snap b/tests-integration/fixtures/checking/102_builtin_row/Main.snap index 6ba87762..7725ef7b 100644 --- a/tests-integration/fixtures/checking/102_builtin_row/Main.snap +++ b/tests-integration/fixtures/checking/102_builtin_row/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms diff --git a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap index 16e6a030..b78ad687 100644 --- a/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap +++ b/tests-integration/fixtures/checking/117_do_ado_constrained/Main.snap @@ -66,15 +66,15 @@ Roles Tuple = [Representational, Representational] Classes -class Functor (&0 :: Type -> Type) -class Functor (&0 :: Type -> Type) <= Apply (&0 :: Type -> Type) -class Apply (&0 :: Type -> Type) <= Applicative (&0 :: Type -> Type) -class Applicative (&0 :: Type -> Type) <= Discard (&0 :: Type -> Type) -class Applicative (&0 :: Type -> Type) <= Bind (&0 :: Type -> Type) -class Bind (&0 :: Type -> Type) <= Monad (&0 :: Type -> Type) +class Functor (f :: Type -> Type) +class Functor (f :: Type -> Type) <= Apply (f :: Type -> Type) +class Apply (f :: Type -> Type) <= Applicative (f :: Type -> Type) +class Applicative (m :: Type -> Type) <= Discard (m :: Type -> Type) +class Applicative (m :: Type -> Type) <= Bind (m :: Type -> Type) +class Bind (m :: Type -> Type) <= Monad (m :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Discard (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Discard (m :: Type -> Type) --> 44:1..44:44 | 44 | testDoDiscard :: forall m. Monad m => m Int diff --git a/tests-integration/fixtures/checking/118_instance_member_type_match/Main.snap b/tests-integration/fixtures/checking/118_instance_member_type_match/Main.snap index 196c0477..fd6040c4 100644 --- a/tests-integration/fixtures/checking/118_instance_member_type_match/Main.snap +++ b/tests-integration/fixtures/checking/118_instance_member_type_match/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,7 +10,7 @@ Types Show :: Type -> Constraint Classes -class Show (&0 :: Type) +class Show (a :: Type) Instances instance Show (Int :: Type) diff --git a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap index b52dd777..219320da 100644 --- a/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap +++ b/tests-integration/fixtures/checking/119_instance_member_type_mismatch/Main.snap @@ -10,7 +10,7 @@ Types Show :: Type -> Constraint Classes -class Show (&0 :: Type) +class Show (a :: Type) Instances instance Show (Int :: Type) diff --git a/tests-integration/fixtures/checking/120_class_explicit_kind_variable/Main.snap b/tests-integration/fixtures/checking/120_class_explicit_kind_variable/Main.snap index 804860aa..8edb4804 100644 --- a/tests-integration/fixtures/checking/120_class_explicit_kind_variable/Main.snap +++ b/tests-integration/fixtures/checking/120_class_explicit_kind_variable/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -11,7 +12,7 @@ Types TypeEq :: forall (k :: Type). Type -> Type -> (k :: Type) -> Constraint Classes -class TypeEq (&1 :: Type) (&2 :: Type) (&3 :: (&0 :: Type)) +class TypeEq (a :: Type) (b :: Type) (r :: (k :: Type)) Instances instance TypeEq (Int :: Type) (Int :: Type) (Int :: Type) diff --git a/tests-integration/fixtures/checking/121_instance_member_inner_forall/Main.snap b/tests-integration/fixtures/checking/121_instance_member_inner_forall/Main.snap index 7aeae948..c01c9463 100644 --- a/tests-integration/fixtures/checking/121_instance_member_inner_forall/Main.snap +++ b/tests-integration/fixtures/checking/121_instance_member_inner_forall/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -25,7 +26,7 @@ Roles Box = [Representational] Classes -class Functor (&0 :: Type -> Type) +class Functor (f :: Type -> Type) Instances instance Functor (Box :: Type -> Type) diff --git a/tests-integration/fixtures/checking/122_instance_member_inner_forall_constraint/Main.snap b/tests-integration/fixtures/checking/122_instance_member_inner_forall_constraint/Main.snap index 8da20ce0..ee30515e 100644 --- a/tests-integration/fixtures/checking/122_instance_member_inner_forall_constraint/Main.snap +++ b/tests-integration/fixtures/checking/122_instance_member_inner_forall_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -36,8 +37,8 @@ Box = [Representational] Maybe = [Representational] Classes -class Show (&0 :: Type) -class Functor (&0 :: Type -> Type) +class Show (a :: Type) +class Functor (f :: Type -> Type) Instances instance Functor (Box :: Type -> Type) diff --git a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap index 4428d144..e732cd17 100644 --- a/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap +++ b/tests-integration/fixtures/checking/123_incomplete_instance_head/Main.snap @@ -13,7 +13,7 @@ Types Pair :: Type -> Type -> Constraint Classes -class Pair (&0 :: Type) (&1 :: Type) +class Pair (a :: Type) (b :: Type) Instances instance Pair (Int :: Type) (String :: Type) diff --git a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap index 400fb72d..f28ff718 100644 --- a/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap +++ b/tests-integration/fixtures/checking/124_instance_member_missing_constraint/Main.snap @@ -28,15 +28,15 @@ Roles Box = [Representational] Classes -class Show (&0 :: Type) -class Functor (&0 :: Type -> Type) +class Show (a :: Type) +class Functor (f :: Type -> Type) Instances instance Functor (Box :: Type -> Type) chain: 0 Diagnostics -error[NoInstanceFound]: No instance found for: Show (~&1 :: Type) +error[NoInstanceFound]: No instance found for: Show (~a :: Type) --> 11:1..16:16 | 11 | instance Functor Box where diff --git a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap index 467ed8e3..d269318c 100644 --- a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap +++ b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap @@ -10,7 +10,7 @@ Types Show :: Type -> Constraint Classes -class Show (&0 :: Type) +class Show (a :: Type) Instances instance Show (Boolean :: Type) diff --git a/tests-integration/fixtures/checking/126_instance_phantom/Main.snap b/tests-integration/fixtures/checking/126_instance_phantom/Main.snap index 99151044..4b40b8e5 100644 --- a/tests-integration/fixtures/checking/126_instance_phantom/Main.snap +++ b/tests-integration/fixtures/checking/126_instance_phantom/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -22,8 +23,8 @@ Roles Proxy = [Phantom] Classes -class Phantom (&0 :: Type) +class Phantom (a :: Type) Instances -instance forall (&0 :: Type). Phantom (Proxy @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type) +instance forall (t2 :: Type). Phantom (Proxy @(t2 :: Type) (a :: (t2 :: Type)) :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap index c76f2e62..32798dd8 100644 --- a/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap +++ b/tests-integration/fixtures/checking/127_derive_eq_simple/Main.snap @@ -33,7 +33,7 @@ NoEq = [] ContainsNoEq = [] Derived -derive forall (&0 :: Type). Eq (Proxy @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type) +derive forall (t1 :: Type). Eq (Proxy @(t1 :: Type) (a :: (t1 :: Type)) :: Type) derive Eq (ContainsNoEq :: Type) Diagnostics diff --git a/tests-integration/fixtures/checking/130_derive_eq_parameterized/Main.snap b/tests-integration/fixtures/checking/130_derive_eq_parameterized/Main.snap index c618f3a5..64ef72c5 100644 --- a/tests-integration/fixtures/checking/130_derive_eq_parameterized/Main.snap +++ b/tests-integration/fixtures/checking/130_derive_eq_parameterized/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -19,4 +20,4 @@ Roles Maybe = [Representational] Derived -derive Eq (&0 :: Type) => Eq (Maybe (&0 :: Type) :: Type) +derive Eq (a :: Type) => Eq (Maybe (a :: Type) :: Type) diff --git a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap index a43e9f9e..886a1a62 100644 --- a/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/132_derive_eq_1_higher_kinded/Main.snap @@ -32,11 +32,11 @@ Wrap = [Representational, Nominal] WrapNoEq1 = [Representational, Nominal] Derived -derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive Eq (&1 :: Type) => Eq (WrapNoEq1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) +derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive Eq (a :: Type) => Eq (WrapNoEq1 @Type (f :: Type -> Type) (a :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Eq1 (f :: Type -> Type) --> 12:1..12:43 | 12 | derive instance Eq a => Eq (WrapNoEq1 f a) diff --git a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap index 7152bcf6..88f4cfab 100644 --- a/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap +++ b/tests-integration/fixtures/checking/133_derive_eq_partial/Main.snap @@ -20,11 +20,11 @@ Roles Either = [Representational, Representational] Derived -derive Eq (&0 :: Type) => Eq (Either Int (&0 :: Type) :: Type) -derive Eq (Either Int (&0 :: Type) :: Type) +derive Eq (b :: Type) => Eq (Either Int (b :: Type) :: Type) +derive Eq (Either Int (b :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq (&0 :: Type) +error[NoInstanceFound]: No instance found for: Eq (b :: Type) --> 9:1..9:34 | 9 | derive instance Eq (Either Int b) diff --git a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap index 3d9665d4..5f019320 100644 --- a/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/135_derive_ord_1_higher_kinded/Main.snap @@ -32,18 +32,18 @@ Wrap = [Representational, Nominal] WrapNoOrd1 = [Representational, Nominal] Derived -derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive Ord (&1 :: Type) => Ord (WrapNoOrd1 @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) +derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive (Ord1 (f :: Type -> Type), Ord (a :: Type)) => Ord (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (WrapNoOrd1 @Type (f :: Type -> Type) (a :: Type) :: Type) +derive Ord (a :: Type) => Ord (WrapNoOrd1 @Type (f :: Type -> Type) (a :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Ord1 (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Ord1 (f :: Type -> Type) --> 14:1..14:46 | 14 | derive instance Ord a => Ord (WrapNoOrd1 f a) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Eq1 (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Eq1 (f :: Type -> Type) --> 14:1..14:46 | 14 | derive instance Ord a => Ord (WrapNoOrd1 f a) diff --git a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap index 9b63086a..541f8ed1 100644 --- a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap @@ -47,32 +47,32 @@ U = [Representational] V = [Representational, Representational] Derived -derive Eq1 (&0 :: Type -> Type) => Eq (V @Type (&0 :: Type -> Type) (&1 :: Int -> Type) :: Type) -derive Ord1 (&0 :: Type -> Type) => Ord (V @Type (&0 :: Type -> Type) (&1 :: Int -> Type) :: Type) -derive Eq1 (&0 :: Type -> Type) => Eq (T @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type) -derive Ord1 (&0 :: Type -> Type) => Ord (T @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type) -derive Eq (&0 :: Type) => Eq (Maybe (&0 :: Type) :: Type) -derive Ord (&0 :: Type) => Ord (Maybe (&0 :: Type) :: Type) -derive Eq1 (&0 :: Type -> Type) => Eq (U (&0 :: Type -> Type) :: Type) -derive Ord1 (&0 :: Type -> Type) => Ord (U (&0 :: Type -> Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) +derive Ord1 (f :: Type -> Type) => Ord (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq (T @Type (f :: Type -> Type) (g :: Type -> Type) :: Type) +derive Ord1 (f :: Type -> Type) => Ord (T @Type (f :: Type -> Type) (g :: Type -> Type) :: Type) +derive Eq (a :: Type) => Eq (Maybe (a :: Type) :: Type) +derive Ord (a :: Type) => Ord (Maybe (a :: Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq (U (f :: Type -> Type) :: Type) +derive Ord1 (f :: Type -> Type) => Ord (U (f :: Type -> Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) Int) +error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) Int) --> 8:1..8:38 | 8 | derive instance (Eq1 f) => Eq (T f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) Int) +error[NoInstanceFound]: No instance found for: Ord ((g :: Type -> Type) Int) --> 9:1..9:40 | 9 | derive instance (Ord1 f) => Ord (T f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Int -> Type) 42) +error[NoInstanceFound]: No instance found for: Eq ((g :: Int -> Type) 42) --> 23:1..23:38 | 23 | derive instance (Eq1 f) => Eq (V f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Ord ((&1 :: Int -> Type) 42) +error[NoInstanceFound]: No instance found for: Ord ((g :: Int -> Type) 42) --> 24:1..24:40 | 24 | derive instance (Ord1 f) => Ord (V f g) diff --git a/tests-integration/fixtures/checking/139_derive_newtype_with_given/Main.snap b/tests-integration/fixtures/checking/139_derive_newtype_with_given/Main.snap index 82d96445..692d35e6 100644 --- a/tests-integration/fixtures/checking/139_derive_newtype_with_given/Main.snap +++ b/tests-integration/fixtures/checking/139_derive_newtype_with_given/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,4 +19,4 @@ Roles Identity = [Representational] Derived -derive Show (&0 :: Type) => Show (Identity (&0 :: Type) :: Type) +derive Show (a :: Type) => Show (Identity (a :: Type) :: Type) diff --git a/tests-integration/fixtures/checking/140_derive_newtype_recursive/Main.snap b/tests-integration/fixtures/checking/140_derive_newtype_recursive/Main.snap index 5e036496..3c7102f0 100644 --- a/tests-integration/fixtures/checking/140_derive_newtype_recursive/Main.snap +++ b/tests-integration/fixtures/checking/140_derive_newtype_recursive/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -19,4 +20,4 @@ Roles Mu = [Representational] Derived -derive Show ((&0 :: Type -> Type) (Mu (&0 :: Type -> Type))) => Show (Mu (&0 :: Type -> Type) :: Type) +derive Show ((f :: Type -> Type) (Mu (f :: Type -> Type))) => Show (Mu (f :: Type -> Type) :: Type) diff --git a/tests-integration/fixtures/checking/141_derive_newtype_phantom/Main.snap b/tests-integration/fixtures/checking/141_derive_newtype_phantom/Main.snap index a1802470..ab38b26f 100644 --- a/tests-integration/fixtures/checking/141_derive_newtype_phantom/Main.snap +++ b/tests-integration/fixtures/checking/141_derive_newtype_phantom/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,4 +19,4 @@ Roles Vector = [Phantom, Representational] Derived -derive Show (&1 :: Type) => Show (Vector (&0 :: Int) (&1 :: Type) :: Type) +derive Show (a :: Type) => Show (Vector (n :: Int) (a :: Type) :: Type) diff --git a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap index ca2a9ff3..cf69a790 100644 --- a/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap +++ b/tests-integration/fixtures/checking/144_derive_newtype_missing_given/Main.snap @@ -19,10 +19,10 @@ Roles Identity = [Representational] Derived -derive Show (Identity (&0 :: Type) :: Type) +derive Show (Identity (a :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Show (&0 :: Type) +error[NoInstanceFound]: No instance found for: Show (a :: Type) --> 7:1..7:42 | 7 | derive newtype instance Show (Identity a) diff --git a/tests-integration/fixtures/checking/146_derive_functor_simple/Main.snap b/tests-integration/fixtures/checking/146_derive_functor_simple/Main.snap index cde36672..89fa3925 100644 --- a/tests-integration/fixtures/checking/146_derive_functor_simple/Main.snap +++ b/tests-integration/fixtures/checking/146_derive_functor_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -36,5 +37,5 @@ Maybe = [Representational] Derived derive Functor (Identity :: Type -> Type) -derive Functor (Const @Type (&0 :: Type) :: Type -> Type) +derive Functor (Const @Type (e :: Type) :: Type -> Type) derive Functor (Maybe :: Type -> Type) diff --git a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap index 00d74b91..5995b641 100644 --- a/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/147_derive_functor_higher_kinded/Main.snap @@ -32,11 +32,11 @@ Wrap = [Representational, Nominal] WrapNoFunctor = [Representational, Nominal] Derived -derive Functor (&0 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) -derive Functor (WrapNoFunctor @Type (&0 :: Type -> Type) :: Type -> Type) +derive Functor (f :: Type -> Type) => Functor (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive Functor (WrapNoFunctor @Type (f :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Functor (f :: Type -> Type) --> 9:1..9:42 | 9 | derive instance Functor (WrapNoFunctor f) diff --git a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap index c4c4bd29..127ad714 100644 --- a/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/148_derive_functor_contravariant_error/Main.snap @@ -37,11 +37,11 @@ Cont = [Representational, Representational] Derived derive Functor (Predicate :: Type -> Type) -derive Functor (Reader (&0 :: Type) :: Type -> Type) -derive Functor (Cont (&0 :: Type) :: Type -> Type) +derive Functor (Reader (r :: Type) :: Type -> Type) +derive Functor (Cont (r :: Type) :: Type -> Type) Diagnostics -error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~&0 :: Type) +error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~a :: Type) --> 6:1..6:34 | 6 | derive instance Functor Predicate diff --git a/tests-integration/fixtures/checking/149_derive_bifunctor_simple/Main.snap b/tests-integration/fixtures/checking/149_derive_bifunctor_simple/Main.snap index c5aab060..4b46692d 100644 --- a/tests-integration/fixtures/checking/149_derive_bifunctor_simple/Main.snap +++ b/tests-integration/fixtures/checking/149_derive_bifunctor_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -38,4 +39,4 @@ Const2 = [Representational, Phantom, Phantom] Derived derive Bifunctor (Either :: Type -> Type -> Type) derive Bifunctor (Pair :: Type -> Type -> Type) -derive Bifunctor (Const2 @Type @Type (&0 :: Type) :: Type -> Type -> Type) +derive Bifunctor (Const2 @Type @Type (e :: Type) :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap index 3742d14e..3b5eb99a 100644 --- a/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/150_derive_bifunctor_higher_kinded/Main.snap @@ -48,16 +48,16 @@ WrapBoth = [Representational, Representational, Nominal, Nominal] WrapBothNoConstraint = [Representational, Representational, Nominal, Nominal] Derived -derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -derive Bifunctor (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) +derive (Functor (f :: Type -> Type), Functor (g :: Type -> Type)) => Bifunctor (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) +derive Bifunctor (WrapBothNoConstraint @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Functor (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Functor (f :: Type -> Type) --> 10:1..10:53 | 10 | derive instance Bifunctor (WrapBothNoConstraint f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Functor (&1 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Functor (g :: Type -> Type) --> 10:1..10:53 | 10 | derive instance Bifunctor (WrapBothNoConstraint f g) diff --git a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap index afff3e01..7abee319 100644 --- a/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap +++ b/tests-integration/fixtures/checking/151_derive_bifunctor_missing_functor/Main.snap @@ -30,7 +30,7 @@ Roles WrapBoth = [Representational, Representational, Nominal, Nominal] Derived -derive Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) +derive Bifunctor (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) Diagnostics error[DeriveMissingFunctor]: Deriving Functor requires Data.Functor to be in scope diff --git a/tests-integration/fixtures/checking/152_derive_contravariant_simple/Main.snap b/tests-integration/fixtures/checking/152_derive_contravariant_simple/Main.snap index fb24b6f7..3827168a 100644 --- a/tests-integration/fixtures/checking/152_derive_contravariant_simple/Main.snap +++ b/tests-integration/fixtures/checking/152_derive_contravariant_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -34,4 +35,4 @@ Op = [Representational, Representational] Derived derive Contravariant (Predicate :: Type -> Type) derive Contravariant (Comparison :: Type -> Type) -derive Contravariant (Op (&0 :: Type) :: Type -> Type) +derive Contravariant (Op (a :: Type) :: Type -> Type) diff --git a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap index 65de6a52..9af13322 100644 --- a/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking/153_derive_contravariant_error/Main.snap @@ -30,12 +30,12 @@ derive Contravariant (Identity :: Type -> Type) derive Contravariant (Producer :: Type -> Type) Diagnostics -error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~a :: Type) --> 7:1..7:39 | 7 | derive instance Contravariant Identity | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~a :: Type) --> 11:1..11:39 | 11 | derive instance Contravariant Producer diff --git a/tests-integration/fixtures/checking/154_derive_profunctor_simple/Main.snap b/tests-integration/fixtures/checking/154_derive_profunctor_simple/Main.snap index 48d1414a..8e978959 100644 --- a/tests-integration/fixtures/checking/154_derive_profunctor_simple/Main.snap +++ b/tests-integration/fixtures/checking/154_derive_profunctor_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -36,5 +37,5 @@ Choice = [Representational, Representational] Derived derive Profunctor (Fn :: Type -> Type -> Type) -derive Profunctor (ConstR @Type (&0 :: Type) :: Type -> Type -> Type) +derive Profunctor (ConstR @Type (r :: Type) :: Type -> Type -> Type) derive Profunctor (Choice :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap index 4edf070e..3112375f 100644 --- a/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking/155_derive_profunctor_error/Main.snap @@ -33,17 +33,17 @@ derive Profunctor (WrongFirst :: Type -> Type -> Type) derive Profunctor (WrongSecond :: Type -> Type -> Type) Diagnostics -error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~a :: Type) --> 7:1..7:38 | 7 | derive instance Profunctor WrongFirst | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~&1 :: Type) +error[ContravariantOccurrence]: Type variable occurs in contravariant position: (~b :: Type) --> 11:1..11:39 | 11 | derive instance Profunctor WrongSecond | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[CovariantOccurrence]: Type variable occurs in covariant position: (~&0 :: Type) +error[CovariantOccurrence]: Type variable occurs in covariant position: (~a :: Type) --> 11:1..11:39 | 11 | derive instance Profunctor WrongSecond diff --git a/tests-integration/fixtures/checking/158_derive_foldable_simple/Main.snap b/tests-integration/fixtures/checking/158_derive_foldable_simple/Main.snap index 0ab900c4..60d87ba7 100644 --- a/tests-integration/fixtures/checking/158_derive_foldable_simple/Main.snap +++ b/tests-integration/fixtures/checking/158_derive_foldable_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -37,4 +38,4 @@ Const = [Representational, Phantom] Derived derive Foldable (Identity :: Type -> Type) derive Foldable (Maybe :: Type -> Type) -derive Foldable (Const @Type (&0 :: Type) :: Type -> Type) +derive Foldable (Const @Type (e :: Type) :: Type -> Type) diff --git a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap index 41195b64..98b51c69 100644 --- a/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/159_derive_foldable_higher_kinded/Main.snap @@ -32,11 +32,11 @@ Wrap = [Representational, Nominal] WrapNoFoldable = [Representational, Nominal] Derived -derive Foldable (&0 :: Type -> Type) => Foldable (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) -derive Foldable (WrapNoFoldable @Type (&0 :: Type -> Type) :: Type -> Type) +derive Foldable (f :: Type -> Type) => Foldable (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive Foldable (WrapNoFoldable @Type (f :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Foldable (f :: Type -> Type) --> 9:1..9:44 | 9 | derive instance Foldable (WrapNoFoldable f) diff --git a/tests-integration/fixtures/checking/160_derive_bifoldable_simple/Main.snap b/tests-integration/fixtures/checking/160_derive_bifoldable_simple/Main.snap index ad951a85..4dca62dc 100644 --- a/tests-integration/fixtures/checking/160_derive_bifoldable_simple/Main.snap +++ b/tests-integration/fixtures/checking/160_derive_bifoldable_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -38,4 +39,4 @@ Const2 = [Representational, Phantom, Phantom] Derived derive Bifoldable (Either :: Type -> Type -> Type) derive Bifoldable (Pair :: Type -> Type -> Type) -derive Bifoldable (Const2 @Type @Type (&0 :: Type) :: Type -> Type -> Type) +derive Bifoldable (Const2 @Type @Type (e :: Type) :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap index f4cc2c0e..0ed8b63f 100644 --- a/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/161_derive_bifoldable_higher_kinded/Main.snap @@ -48,16 +48,16 @@ WrapBoth = [Representational, Representational, Nominal, Nominal] WrapBothNoConstraint = [Representational, Representational, Nominal, Nominal] Derived -derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Bifoldable (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -derive Bifoldable (WrapBothNoConstraint @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) +derive (Foldable (f :: Type -> Type), Foldable (g :: Type -> Type)) => Bifoldable (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) +derive Bifoldable (WrapBothNoConstraint @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Foldable (&0 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Foldable (f :: Type -> Type) --> 10:1..10:54 | 10 | derive instance Bifoldable (WrapBothNoConstraint f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Foldable (&1 :: Type -> Type) +error[NoInstanceFound]: No instance found for: Foldable (g :: Type -> Type) --> 10:1..10:54 | 10 | derive instance Bifoldable (WrapBothNoConstraint f g) diff --git a/tests-integration/fixtures/checking/162_derive_traversable_simple/Main.snap b/tests-integration/fixtures/checking/162_derive_traversable_simple/Main.snap index f21c15a2..63b92027 100644 --- a/tests-integration/fixtures/checking/162_derive_traversable_simple/Main.snap +++ b/tests-integration/fixtures/checking/162_derive_traversable_simple/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -41,6 +42,6 @@ derive Traversable (Identity :: Type -> Type) derive Functor (Maybe :: Type -> Type) derive Foldable (Maybe :: Type -> Type) derive Traversable (Maybe :: Type -> Type) -derive Functor (Const @Type (&0 :: Type) :: Type -> Type) -derive Foldable (Const @Type (&0 :: Type) :: Type -> Type) -derive Traversable (Const @Type (&0 :: Type) :: Type -> Type) +derive Functor (Const @Type (e :: Type) :: Type -> Type) +derive Foldable (Const @Type (e :: Type) :: Type -> Type) +derive Traversable (Const @Type (e :: Type) :: Type -> Type) diff --git a/tests-integration/fixtures/checking/163_derive_traversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/163_derive_traversable_higher_kinded/Main.snap index 52ca615b..ac6d020c 100644 --- a/tests-integration/fixtures/checking/163_derive_traversable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/163_derive_traversable_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -27,6 +28,6 @@ Roles Compose = [Representational, Representational, Nominal] Derived -derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Traversable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) +derive (Functor (f :: Type -> Type), Functor (g :: Type -> Type)) => Functor (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive (Foldable (f :: Type -> Type), Foldable (g :: Type -> Type)) => Foldable (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive (Traversable (f :: Type -> Type), Traversable (g :: Type -> Type)) => Traversable (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) diff --git a/tests-integration/fixtures/checking/165_derive_bitraversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking/165_derive_bitraversable_higher_kinded/Main.snap index e3785f03..fef2082d 100644 --- a/tests-integration/fixtures/checking/165_derive_bitraversable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/165_derive_bitraversable_higher_kinded/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -29,6 +30,6 @@ Roles WrapBoth = [Representational, Representational, Nominal, Nominal] Derived -derive (Functor (&0 :: Type -> Type), Functor (&1 :: Type -> Type)) => Bifunctor (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -derive (Foldable (&0 :: Type -> Type), Foldable (&1 :: Type -> Type)) => Bifoldable (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) -derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Bitraversable (WrapBoth @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type -> Type) +derive (Functor (f :: Type -> Type), Functor (g :: Type -> Type)) => Bifunctor (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) +derive (Foldable (f :: Type -> Type), Foldable (g :: Type -> Type)) => Bifoldable (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) +derive (Traversable (f :: Type -> Type), Traversable (g :: Type -> Type)) => Bitraversable (WrapBoth @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap index f2afb618..9003689b 100644 --- a/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap +++ b/tests-integration/fixtures/checking/166_derive_traversable_missing_superclass/Main.snap @@ -28,15 +28,15 @@ Roles Compose = [Representational, Representational, Nominal] Derived -derive (Traversable (&0 :: Type -> Type), Traversable (&1 :: Type -> Type)) => Traversable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) +derive (Traversable (f :: Type -> Type), Traversable (g :: Type -> Type)) => Traversable (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Functor (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound]: No instance found for: Functor (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type)) --> 7:1..7:76 | 7 | derive instance (Traversable f, Traversable g) => Traversable (Compose f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Foldable (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type)) +error[NoInstanceFound]: No instance found for: Foldable (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type)) --> 7:1..7:76 | 7 | derive instance (Traversable f, Traversable g) => Traversable (Compose f g) diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index e9377dfc..6f001319 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -81,29 +81,29 @@ Either' = [Representational] NoEq = [Representational] Derived -derive Eq1 (&0 :: Type -> Type) => Eq1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) -derive forall (&0 :: Type). (Eq1 (&1 :: Type -> Type), Eq ((&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)))) => Eq (Compose @Type @(&0 :: Type) (&1 :: Type -> Type) (&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)) :: Type) -derive Eq1 (&0 :: Type -> Type) => Eq1 (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -derive Eq (&0 :: Type) => Eq (Id (&0 :: Type) :: Type) -derive Eq (&0 :: Type) => Eq (Either' (&0 :: Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive forall (t41 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)))) => Eq (Compose @Type @(t41 :: Type) (f :: Type -> Type) (g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Eq (a :: Type) => Eq (Id (a :: Type) :: Type) +derive Eq (a :: Type) => Eq (Either' (a :: Type) :: Type) derive Eq1 (Either' :: Type -> Type) derive Eq1 (NoEq :: Type -> Type) derive Eq1 (Id :: Type -> Type) -derive Eq (&0 :: Type) => Eq (Pair (&0 :: Type) :: Type) +derive Eq (a :: Type) => Eq (Pair (a :: Type) :: Type) derive Eq1 (Pair :: Type -> Type) -derive Eq (&0 :: Type) => Eq (Mixed (&0 :: Type) :: Type) +derive Eq (a :: Type) => Eq (Mixed (a :: Type) :: Type) derive Eq1 (Mixed :: Type -> Type) -derive Eq (&0 :: Type) => Eq (Rec (&0 :: Type) :: Type) +derive Eq (a :: Type) => Eq (Rec (a :: Type) :: Type) derive Eq1 (Rec :: Type -> Type) -derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) +derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) (~_ :: Type)) --> 35:1..35:43 | 35 | derive instance Eq1 f => Eq1 (Compose f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Eq (NoEq (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Eq (NoEq (~_ :: Type)) --> 45:1..45:25 | 45 | derive instance Eq1 NoEq diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index d669c541..080b13c9 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -52,34 +52,34 @@ Compose = [Representational, Representational, Nominal] NoOrd = [Representational] Derived -derive forall (&0 :: Type). (Eq1 (&1 :: Type -> Type), Eq ((&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)))) => Eq (Compose @Type @(&0 :: Type) (&1 :: Type -> Type) (&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)) :: Type) -derive forall (&0 :: Type). (Ord1 (&1 :: Type -> Type), Ord ((&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)))) => Ord (Compose @Type @(&0 :: Type) (&1 :: Type -> Type) (&2 :: (&0 :: Type) -> Type) (&3 :: (&0 :: Type)) :: Type) -derive Eq1 (&0 :: Type -> Type) => Eq1 (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -derive Ord1 (&0 :: Type -> Type) => Ord1 (Compose @Type @Type (&0 :: Type -> Type) (&1 :: Type -> Type) :: Type -> Type) -derive Eq (&0 :: Type) => Eq (NoOrd (&0 :: Type) :: Type) +derive forall (t40 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)))) => Eq (Compose @Type @(t40 :: Type) (f :: Type -> Type) (g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)) :: Type) +derive forall (t48 :: Type). (Ord1 (f :: Type -> Type), Ord ((g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)))) => Ord (Compose @Type @(t48 :: Type) (f :: Type -> Type) (g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Ord1 (f :: Type -> Type) => Ord1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Eq (a :: Type) => Eq (NoOrd (a :: Type) :: Type) derive Eq1 (NoOrd :: Type -> Type) derive Ord1 (NoOrd :: Type -> Type) -derive Eq (&0 :: Type) => Eq (Id (&0 :: Type) :: Type) +derive Eq (a :: Type) => Eq (Id (a :: Type) :: Type) derive Eq1 (Id :: Type -> Type) -derive Ord (&0 :: Type) => Ord (Id (&0 :: Type) :: Type) +derive Ord (a :: Type) => Ord (Id (a :: Type) :: Type) derive Ord1 (Id :: Type -> Type) -derive (Eq1 (&0 :: Type -> Type), Eq (&1 :: Type)) => Eq (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive Eq1 (&0 :: Type -> Type) => Eq1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) -derive (Ord1 (&0 :: Type -> Type), Ord (&1 :: Type)) => Ord (Wrap @Type (&0 :: Type -> Type) (&1 :: Type) :: Type) -derive Ord1 (&0 :: Type -> Type) => Ord1 (Wrap @Type (&0 :: Type -> Type) :: Type -> Type) +derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive (Ord1 (f :: Type -> Type), Ord (a :: Type)) => Ord (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive Ord1 (f :: Type -> Type) => Ord1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) Diagnostics -error[NoInstanceFound]: No instance found for: Eq ((&1 :: Type -> Type) (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) (~_ :: Type)) --> 26:1..26:43 | 26 | derive instance Eq1 f => Eq1 (Compose f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Ord ((&1 :: Type -> Type) (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Ord ((g :: Type -> Type) (~_ :: Type)) --> 27:1..27:45 | 27 | derive instance Ord1 f => Ord1 (Compose f g) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -error[NoInstanceFound]: No instance found for: Ord (NoOrd (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Ord (NoOrd (~_ :: Type)) --> 34:1..34:27 | 34 | derive instance Ord1 NoOrd diff --git a/tests-integration/fixtures/checking/170_derive_newtype_class_parameterized/Main.snap b/tests-integration/fixtures/checking/170_derive_newtype_class_parameterized/Main.snap index 54ed7909..5ae9287b 100644 --- a/tests-integration/fixtures/checking/170_derive_newtype_class_parameterized/Main.snap +++ b/tests-integration/fixtures/checking/170_derive_newtype_class_parameterized/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -18,4 +19,4 @@ Roles Wrapper = [Representational] Derived -derive Newtype (Wrapper (&0 :: Type) :: Type) ((&0 :: Type) :: Type) +derive Newtype (Wrapper (a :: Type) :: Type) ((a :: Type) :: Type) diff --git a/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap b/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap index e103bb7b..860b4caa 100644 --- a/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap +++ b/tests-integration/fixtures/checking/172_derive_generic_simple/Main.snap @@ -92,7 +92,7 @@ Proxy = [Phantom] Derived derive Generic (Void :: Type) (NoConstructors :: Type) derive Generic (MyUnit :: Type) (Constructor "MyUnit" NoArguments :: Type) -derive Generic (Identity (&0 :: Type) :: Type) (Constructor "Identity" (Argument (&0 :: Type)) :: Type) -derive Generic (Either (&0 :: Type) (&1 :: Type) :: Type) (Sum (Constructor "Left" (Argument (&0 :: Type))) (Constructor "Right" (Argument (&1 :: Type))) :: Type) -derive Generic (Tuple (&0 :: Type) (&1 :: Type) :: Type) (Constructor "Tuple" (Product (Argument (&0 :: Type)) (Argument (&1 :: Type))) :: Type) -derive Generic (Wrapper (&0 :: Type) :: Type) (Constructor "Wrapper" (Argument (&0 :: Type)) :: Type) +derive Generic (Identity (a :: Type) :: Type) (Constructor "Identity" (Argument (a :: Type)) :: Type) +derive Generic (Either (a :: Type) (b :: Type) :: Type) (Sum (Constructor "Left" (Argument (a :: Type))) (Constructor "Right" (Argument (b :: Type))) :: Type) +derive Generic (Tuple (a :: Type) (b :: Type) :: Type) (Constructor "Tuple" (Product (Argument (a :: Type)) (Argument (b :: Type))) :: Type) +derive Generic (Wrapper (a :: Type) :: Type) (Constructor "Wrapper" (Argument (a :: Type)) :: Type) diff --git a/tests-integration/fixtures/checking/173_derive_newtype_class_coercible/Main.snap b/tests-integration/fixtures/checking/173_derive_newtype_class_coercible/Main.snap index 0ff2c93d..51091999 100644 --- a/tests-integration/fixtures/checking/173_derive_newtype_class_coercible/Main.snap +++ b/tests-integration/fixtures/checking/173_derive_newtype_class_coercible/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -30,4 +31,4 @@ Wrapper = [Representational] Derived derive Newtype (UserId :: Type) (Int :: Type) -derive Newtype (Wrapper (&0 :: Type) :: Type) ((&0 :: Type) :: Type) +derive Newtype (Wrapper (a :: Type) :: Type) ((a :: Type) :: Type) diff --git a/tests-integration/fixtures/checking/176_role_inference_nominal_constraint/Main.snap b/tests-integration/fixtures/checking/176_role_inference_nominal_constraint/Main.snap index c6ca57a0..91b251bf 100644 --- a/tests-integration/fixtures/checking/176_role_inference_nominal_constraint/Main.snap +++ b/tests-integration/fixtures/checking/176_role_inference_nominal_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,4 +22,4 @@ Roles Shown = [Nominal] Classes -class Show (&0 :: Type) +class Show (a :: Type) diff --git a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap index 7a4d589c..305c5398 100644 --- a/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap +++ b/tests-integration/fixtures/checking/197_coercible_higher_kinded_error/Main.snap @@ -31,7 +31,7 @@ List = [Representational] Container = [Representational] Diagnostics -error[NoInstanceFound]: No instance found for: Coercible (Maybe (~&0 :: Type)) (List (~&0 :: Type)) +error[NoInstanceFound]: No instance found for: Coercible (Maybe (~_ :: Type)) (List (~_ :: Type)) --> 11:1..11:62 | 11 | coerceContainerDifferent :: Container Maybe -> Container List diff --git a/tests-integration/fixtures/checking/207_operator_class_method/Main.snap b/tests-integration/fixtures/checking/207_operator_class_method/Main.snap index a59f0462..40280979 100644 --- a/tests-integration/fixtures/checking/207_operator_class_method/Main.snap +++ b/tests-integration/fixtures/checking/207_operator_class_method/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,7 +14,7 @@ Types Semiring :: Type -> Constraint Classes -class Semiring (&0 :: Type) +class Semiring (a :: Type) Instances instance Semiring (Int :: Type) diff --git a/tests-integration/fixtures/checking/208_int_add_constraint/Main.snap b/tests-integration/fixtures/checking/208_int_add_constraint/Main.snap index 7319bae4..84a1b166 100644 --- a/tests-integration/fixtures/checking/208_int_add_constraint/Main.snap +++ b/tests-integration/fixtures/checking/208_int_add_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,8 +14,8 @@ Types Program :: forall (t0 :: Type) (t1 :: Type). (t0 :: Type) -> (t1 :: Type) -> Constraint Classes -class Program (&2 :: (&0 :: Type)) (&3 :: (&1 :: Type)) +class Program (n :: (t0 :: Type)) (m :: (t1 :: Type)) Instances -instance (Add (&0 :: Int) 1 (&2 :: Int), Add (&2 :: Int) 1 (&1 :: Int)) => Program ((&0 :: Int) :: Int) ((&1 :: Int) :: Int) +instance (Add (n :: Int) 1 (n1 :: Int), Add (n1 :: Int) 1 (n2 :: Int)) => Program ((n :: Int) :: Int) ((n2 :: Int) :: Int) chain: 0 diff --git a/tests-integration/fixtures/checking/209_int_cons_constraint/Main.snap b/tests-integration/fixtures/checking/209_int_cons_constraint/Main.snap index 45072ba0..8b2a166a 100644 --- a/tests-integration/fixtures/checking/209_int_cons_constraint/Main.snap +++ b/tests-integration/fixtures/checking/209_int_cons_constraint/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,10 +14,14 @@ Types Build :: forall (t0 :: Type) (t1 :: Type). (t0 :: Type) -> (t1 :: Type) -> Constraint Classes -class Build (&2 :: (&0 :: Type)) (&3 :: (&1 :: Type)) +class Build (n :: (t0 :: Type)) (r :: (t1 :: Type)) Instances -instance forall (&0 :: Type). Build (0 :: Int) (() :: Row (&0 :: Type)) +instance forall (t14 :: Type). Build (0 :: Int) (() :: Row (t14 :: Type)) chain: 0 -instance (Add (&2 :: Int) 1 (&0 :: Int), ToString (&0 :: Int) (&3 :: Symbol), Append "n" (&3 :: Symbol) (&4 :: Symbol), Build @Int @(Row Int) (&2 :: Int) (&5 :: Row Int), Cons @Int (&4 :: Symbol) (&0 :: Int) (&5 :: Row Int) (&1 :: Row Int)) => Build ((&0 :: Int) :: Int) ((&1 :: Row Int) :: Row Int) +instance (Add (minusOne :: Int) 1 (currentId :: Int), ToString (currentId :: Int) (labelId :: Symbol), Append "n" (labelId :: Symbol) (actualLabel :: Symbol), Build @Int @(Row Int) (minusOne :: Int) (minusOneResult :: Row Int), Cons @Int + (actualLabel :: Symbol) + (currentId :: Int) + (minusOneResult :: Row Int) + (finalResult :: Row Int)) => Build ((currentId :: Int) :: Int) ((finalResult :: Row Int) :: Row Int) chain: 1 diff --git a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap index 5f8cf754..d72ef8e9 100644 --- a/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap +++ b/tests-integration/fixtures/checking/220_do_let_premature_solve/Main.snap @@ -26,7 +26,7 @@ Effect = [Nominal] Unit = [] Classes -class Semiring (&0 :: Type) +class Semiring (a :: Type) Instances instance Semiring (Int :: Type) diff --git a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap index 7f3d205d..288fca5c 100644 --- a/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap +++ b/tests-integration/fixtures/checking/222_ado_let_premature_solve/Main.snap @@ -28,7 +28,7 @@ Effect = [Nominal] Unit = [] Classes -class Semiring (&0 :: Type) +class Semiring (a :: Type) Instances instance Semiring (Int :: Type) diff --git a/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap index b7f558ec..0cc40bdc 100644 --- a/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap +++ b/tests-integration/fixtures/checking/232_instance_head_nil_kind_application/Main.snap @@ -10,8 +10,8 @@ ListToRow :: RowList Type -> Constraint ListToRow2 :: RowList Type -> RowList Type -> Constraint Classes -class ListToRow (&0 :: RowList Type) -class ListToRow2 (&0 :: RowList Type) (&1 :: RowList Type) +class ListToRow (xs :: RowList Type) +class ListToRow2 (xs :: RowList Type) (ys :: RowList Type) Instances instance ListToRow (Nil @Type :: RowList Type) diff --git a/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap index f8bf6fd0..9f40bc18 100644 --- a/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap +++ b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap @@ -15,11 +15,11 @@ Make :: Type -> Type -> Constraint Convert :: Type -> Type -> Constraint Classes -class Make (&0 :: Type) (&1 :: Type) -class Convert (&0 :: Type) (&1 :: Type) +class Make (a :: Type) (b :: Type) +class Convert (a :: Type) (b :: Type) Instances -instance Make ({ | (&0 :: Row Type) } :: Type) ({ | (&0 :: Row Type) } :: Type) +instance Make ({ | (r :: Row Type) } :: Type) ({ | (r :: Row Type) } :: Type) chain: 0 -instance Convert ({ | (&0 :: Row Type) } :: Type) ({ converted :: { | (&0 :: Row Type) } } :: Type) +instance Convert ({ | (r :: Row Type) } :: Type) ({ converted :: { | (r :: Row Type) } } :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap index 4246fda0..dc0138cb 100644 --- a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap +++ b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap @@ -16,11 +16,11 @@ Clone :: Type -> Type -> Constraint Nest :: Type -> Type -> Constraint Classes -class Clone (&0 :: Type) (&1 :: Type) -class Nest (&0 :: Type) (&1 :: Type) +class Clone (a :: Type) (b :: Type) +class Nest (a :: Type) (b :: Type) Instances -instance Clone ({ | (&0 :: Row Type) } :: Type) ({ | (&0 :: Row Type) } :: Type) +instance Clone ({ | (r :: Row Type) } :: Type) ({ | (r :: Row Type) } :: Type) chain: 0 -instance Nest ({ | (&0 :: Row Type) } :: Type) ({ inner :: { | (&0 :: Row Type) }, outer :: Int } :: Type) +instance Nest ({ | (r :: Row Type) } :: Type) ({ inner :: { | (r :: Row Type) }, outer :: Int } :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap index 2c12f568..631bd92a 100644 --- a/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap +++ b/tests-integration/fixtures/checking/235_instance_head_invalid_row/Main.snap @@ -20,7 +20,7 @@ Roles Proxy = [Phantom] Classes -class T (&1 :: (&0 :: Type)) +class T (a :: (k :: Type)) Instances instance T (( a :: Int ) :: Row Type) diff --git a/tests-integration/fixtures/checking/236_category_function_instance/Main.snap b/tests-integration/fixtures/checking/236_category_function_instance/Main.snap index a1b96504..c28a8d68 100644 --- a/tests-integration/fixtures/checking/236_category_function_instance/Main.snap +++ b/tests-integration/fixtures/checking/236_category_function_instance/Main.snap @@ -23,8 +23,8 @@ Semigroupoid :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Cons Category :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint Classes -class Semigroupoid (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) -class Semigroupoid @(&0 :: Type) (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) <= Category (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) +class Semigroupoid (a :: (k :: Type) -> (k :: Type) -> Type) +class Semigroupoid @(k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) <= Category (a :: (k :: Type) -> (k :: Type) -> Type) Instances instance Semigroupoid (Function :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap index c4d5dadb..b2cac7d3 100644 --- a/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap +++ b/tests-integration/fixtures/checking/237_bound_variable_unification/Main.snap @@ -18,8 +18,8 @@ Types BuildRecord :: Row Type -> Row Type -> Constraint Classes -class BuildRecord (&0 :: Row Type) (&1 :: Row Type) +class BuildRecord (row :: Row Type) (subrow :: Row Type) Instances -instance BuildRecord ((&0 :: Row Type) :: Row Type) ((&1 :: Row Type) :: Row Type) +instance BuildRecord ((row :: Row Type) :: Row Type) ((subrow :: Row Type) :: Row Type) chain: 0 diff --git a/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap b/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap index eee88fc1..6d2d4bc8 100644 --- a/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap +++ b/tests-integration/fixtures/checking/238_function_application_subtype/Main.snap @@ -15,7 +15,7 @@ Types Category :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint Classes -class Category (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) +class Category (a :: (k :: Type) -> (k :: Type) -> Type) Instances instance Category (Function :: Type -> Type -> Type) diff --git a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap index edaddd8f..dede4dd3 100644 --- a/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap +++ b/tests-integration/fixtures/checking/251_lookup_implicit_panic/Main.snap @@ -10,10 +10,10 @@ Types Identity :: Type -> Constraint Classes -class Identity (&0 :: Type) +class Identity (a :: Type) Instances -instance forall (&0 :: Type) (&1 :: ???). Identity (??? (&2 :: (&1 :: (&0 :: Type))) :: ???) +instance forall (t2 :: ???) (t1 :: Type). Identity (??? (a :: (t2 :: (t1 :: Type))) :: ???) chain: 0 Diagnostics @@ -22,7 +22,7 @@ error[NotInScope]: 'Undefined' is not in scope | 8 | instance Identity (Undefined a) where | ^~~~~~~~~ -error[InvalidTypeApplication]: Cannot apply type '???' to '(&0 :: ?2[:0])'. '???' has kind '???', which is not a function kind. +error[InvalidTypeApplication]: Cannot apply type '???' to '(a :: ?2[:0])'. '???' has kind '???', which is not a function kind. --> 8:20..8:31 | 8 | instance Identity (Undefined a) where diff --git a/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap index 2d9858e1..0f1cb48e 100644 --- a/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap +++ b/tests-integration/fixtures/checking/254_higher_rank_elaboration/Main.snap @@ -28,4 +28,4 @@ Roles Proxy = [Phantom] Classes -class IsSymbol (&0 :: Symbol) +class IsSymbol (sym :: Symbol) diff --git a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap index b631fa3b..c026af80 100644 --- a/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap +++ b/tests-integration/fixtures/checking/269_instance_equation_exhaustive/Main.snap @@ -10,7 +10,7 @@ Types C :: Type -> Constraint Classes -class C (&0 :: Type) +class C (a :: Type) Instances instance C (Boolean :: Type) diff --git a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap index d61b7443..5391a7ce 100644 --- a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap +++ b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap @@ -43,5 +43,5 @@ Roles Unit = [] Classes -class Monoid (&0 :: Type) -class Semigroup (&0 :: Type) +class Monoid (a :: Type) +class Semigroup (a :: Type) diff --git a/tests-integration/fixtures/checking/274_givens_retained/Main.snap b/tests-integration/fixtures/checking/274_givens_retained/Main.snap index 41b4ae1b..6dfb4f73 100644 --- a/tests-integration/fixtures/checking/274_givens_retained/Main.snap +++ b/tests-integration/fixtures/checking/274_givens_retained/Main.snap @@ -11,4 +11,4 @@ Types Given :: Type -> Constraint Classes -class Given (&0 :: Type) +class Given (a :: Type) diff --git a/tests-integration/fixtures/checking/275_givens_scoped/Main.snap b/tests-integration/fixtures/checking/275_givens_scoped/Main.snap index 961b0fa8..fef05ae0 100644 --- a/tests-integration/fixtures/checking/275_givens_scoped/Main.snap +++ b/tests-integration/fixtures/checking/275_givens_scoped/Main.snap @@ -11,7 +11,7 @@ Types Given :: Type -> Constraint Classes -class Given (&0 :: Type) +class Given (a :: Type) Diagnostics error[NoInstanceFound]: No instance found for: Given Int diff --git a/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap index dc23e4cc..23737b72 100644 --- a/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap +++ b/tests-integration/fixtures/checking/281_sectioned_constraint_generation/Main.snap @@ -22,4 +22,4 @@ Roles Unit = [] Classes -class Example (&0 :: Type) +class Example (a :: Type) diff --git a/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap index 1066cac5..6cef9066 100644 --- a/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/285_derive_newtype_higher_kinded/Main.snap @@ -21,7 +21,7 @@ Roles Wrapper = [Representational] Classes -class Empty (&0 :: Type -> Type) +class Empty (f :: Type -> Type) Instances instance Empty (Array :: Type -> Type) diff --git a/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap index e4c7b00e..8973c82f 100644 --- a/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap +++ b/tests-integration/fixtures/checking/286_invalid_vector_newtype_derive/Main.snap @@ -32,14 +32,14 @@ Vector = [Phantom, Representational] InvalidVector = [Representational, Phantom] Classes -class Empty (&0 :: Type -> Type) +class Empty (f :: Type -> Type) Instances instance Empty (Array :: Type -> Type) chain: 0 Derived -derive forall (&0 :: Type). Empty (Vector @(&0 :: Type) (&1 :: (&0 :: Type)) :: Type -> Type) +derive forall (t9 :: Type). Empty (Vector @(t9 :: Type) (n :: (t9 :: Type)) :: Type -> Type) Diagnostics error[InvalidNewtypeDeriveSkolemArguments]: Cannot derive newtype instance where skolemised arguments do not appear trailing in the inner type. diff --git a/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap index e63599b1..ae7b7c9f 100644 --- a/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap +++ b/tests-integration/fixtures/checking/297_applied_function_type_decomposition/Main.snap @@ -53,11 +53,11 @@ Maybe = [Representational] NonEmpty = [Representational, Nominal] Classes -class Foldable (&0 :: Type -> Type) -class Foldable (&1 :: Type -> Type) <= FoldableWithIndex (&0 :: Type) (&1 :: Type -> Type) +class Foldable (f :: Type -> Type) +class Foldable (f :: Type -> Type) <= FoldableWithIndex (i :: Type) (f :: Type -> Type) Instances -instance Foldable (&0 :: Type -> Type) => Foldable (NonEmpty (&0 :: Type -> Type) :: Type -> Type) +instance Foldable (f :: Type -> Type) => Foldable (NonEmpty (f :: Type -> Type) :: Type -> Type) chain: 0 -instance FoldableWithIndex (&0 :: Type) (&1 :: Type -> Type) => FoldableWithIndex (Maybe (&0 :: Type) :: Type) (NonEmpty (&1 :: Type -> Type) :: Type -> Type) +instance FoldableWithIndex (i :: Type) (f :: Type -> Type) => FoldableWithIndex (Maybe (i :: Type) :: Type) (NonEmpty (f :: Type -> Type) :: Type -> Type) chain: 0 diff --git a/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap index 626e969c..212a7bfd 100644 --- a/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap +++ b/tests-integration/fixtures/checking/300_instance_shift_variables/Main.snap @@ -22,5 +22,5 @@ Roles Wrap = [Phantom, Representational, Nominal] Instances -instance Functor (&1 :: Type -> Type) => Functor (Wrap @Type (&0 :: Type) (&1 :: Type -> Type) :: Type -> Type) +instance Functor (w :: Type -> Type) => Functor (Wrap @Type (e :: Type) (w :: Type -> Type) :: Type -> Type) chain: 0 diff --git a/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap index aaebe528..736dfd75 100644 --- a/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap +++ b/tests-integration/fixtures/checking/303_instance_given_constraint/Main.snap @@ -44,8 +44,8 @@ Roles ReaderT = [Representational, Representational, Nominal] Classes -class Functor (&1 :: Type -> Type), Functor (&0 :: Type -> Type) <= Parallel (&0 :: Type -> Type) (&1 :: Type -> Type) +class Functor (m :: Type -> Type), Functor (f :: Type -> Type) <= Parallel (f :: Type -> Type) (m :: Type -> Type) Instances -instance Parallel (&1 :: Type -> Type) (&2 :: Type -> Type) => Parallel (ReaderT (&0 :: Type) (&1 :: Type -> Type) :: Type -> Type) (ReaderT (&0 :: Type) (&2 :: Type -> Type) :: Type -> Type) +instance Parallel (f :: Type -> Type) (m :: Type -> Type) => Parallel (ReaderT (e :: Type) (f :: Type -> Type) :: Type -> Type) (ReaderT (e :: Type) (m :: Type -> Type) :: Type -> Type) chain: 0 diff --git a/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap index 58879edd..cc7e2a34 100644 --- a/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap +++ b/tests-integration/fixtures/checking/306_kind_application_instance_matching/Main.snap @@ -23,5 +23,5 @@ Roles Endo = [Representational, Nominal] Instances -instance forall (&0 :: Type). Newtype (Endo @(&0 :: Type) (&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) (&2 :: (&0 :: Type)) :: Type) ((&1 :: (&0 :: Type) -> (&0 :: Type) -> Type) (&2 :: (&0 :: Type)) (&2 :: (&0 :: Type)) :: Type) +instance forall (t4 :: Type). Newtype (Endo @(t4 :: Type) (c :: (t4 :: Type) -> (t4 :: Type) -> Type) (a :: (t4 :: Type)) :: Type) ((c :: (t4 :: Type) -> (t4 :: Type) -> Type) (a :: (t4 :: Type)) (a :: (t4 :: Type)) :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap b/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap index a7ba5715..09c47702 100644 --- a/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap +++ b/tests-integration/fixtures/checking/307_where_let_interaction/Main.snap @@ -12,7 +12,7 @@ Types MyClass :: Type -> Constraint Classes -class MyClass (&0 :: Type) +class MyClass (a :: Type) Instances instance MyClass (Int :: Type) diff --git a/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap index 3c25ed80..2047c008 100644 --- a/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap +++ b/tests-integration/fixtures/checking/308_let_constraint_scoping/Main.snap @@ -11,7 +11,7 @@ Types MyClass :: Type -> Constraint Classes -class MyClass (&0 :: Type) +class MyClass (a :: Type) Instances instance MyClass (Int :: Type) diff --git a/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap index 1a23bd24..8b9bc333 100644 --- a/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap +++ b/tests-integration/fixtures/checking/313_guarded_constraint_propagation/Main.snap @@ -26,7 +26,7 @@ Roles List = [Representational] Classes -class Generate (&0 :: Type -> Type) +class Generate (f :: Type -> Type) Instances instance Generate (List :: Type -> Type) diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 9edef9d8..e3b455db 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -377,12 +377,11 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { class_line.push_str(name); - // Print class type variables with their kinds - // level = quantified_variables + kind_variables + index (matches localize_class) - for (index, &kind) in class.type_variable_kinds.iter().enumerate() { - let level = class.quantified_variables.0 + class.kind_variables.0 + index as u32; + // Print class type variables with their kinds. + for (name, &kind) in class.type_variable_names.iter().zip(class.type_variable_kinds.iter()) + { let kind_str = pretty::print_global(engine, kind); - class_line.push_str(&format!(" (&{level} :: {kind_str})")); + class_line.push_str(&format!(" ({} :: {kind_str})", name.text)); } writeln!(snapshot, "class {class_line}").unwrap(); @@ -500,16 +499,18 @@ fn format_instance_head( head } -fn format_forall_prefix(engine: &QueryEngine, kind_variables: &[checking::core::TypeId]) -> String { +fn format_forall_prefix( + engine: &QueryEngine, + kind_variables: &[(checking::core::Name, checking::core::TypeId)], +) -> String { if kind_variables.is_empty() { return String::new(); } let binders: Vec<_> = kind_variables .iter() - .enumerate() - .map(|(i, kind)| { + .map(|(name, kind)| { let kind_str = pretty::print_global(engine, *kind); - format!("(&{i} :: {kind_str})") + format!("({} :: {kind_str})", name.text) }) .collect(); format!("forall {}. ", binders.join(" ")) diff --git a/tests-integration/tests/checking.rs b/tests-integration/tests/checking.rs index 0f663fb8..a962ebdf 100644 --- a/tests-integration/tests/checking.rs +++ b/tests-integration/tests/checking.rs @@ -7,7 +7,9 @@ use std::num::NonZeroU32; use analyzer::{QueryEngine, prim}; use checking::algorithm::state::{CheckContext, CheckState, UnificationState}; use checking::algorithm::{quantify, unification}; -use checking::core::{ForallBinder, RowField, RowType, Type, TypeId, Variable, debruijn, pretty}; +use checking::core::{ + ForallBinder, Name, RowField, RowType, Type, TypeId, Variable, debruijn, pretty, +}; use files::{FileId, Files}; use itertools::Itertools; use lowering::TypeVariableBindingId; @@ -19,21 +21,21 @@ struct ContextState<'r> { impl<'a> ContextState<'a> { fn new(engine: &'a QueryEngine, id: FileId) -> ContextState<'a> { - let mut state = CheckState::default(); + let mut state = CheckState::new(id); let context = CheckContext::new(engine, &mut state, id).unwrap(); ContextState { state, context } } } trait CheckStateExt { - fn bound_variable(&mut self, index: u32, kind: TypeId) -> TypeId; + fn bound_variable(&mut self, name: Name, kind: TypeId) -> TypeId; fn function(&mut self, argument: TypeId, result: TypeId) -> TypeId; } impl CheckStateExt for CheckState { - fn bound_variable(&mut self, index: u32, kind: TypeId) -> TypeId { - let var = Variable::Bound(debruijn::Level(index), kind); + fn bound_variable(&mut self, name: Name, kind: TypeId) -> TypeId { + let var = Variable::Bound(name, kind); self.storage.intern(Type::Variable(var)) } @@ -63,8 +65,18 @@ fn test_solve_simple() { let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // [a :: Int, b :: String] - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.int); - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); + let name_a = state.fresh_name_str("a"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.int, + name_a, + ); + let name_b = state.fresh_name_str("b"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.string, + name_b, + ); let unification = state.fresh_unification_type(context); let Type::Unification(unification_id) = state.storage[unification] else { @@ -91,16 +103,26 @@ fn test_solve_bound() { let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // [a :: Int, b :: String] - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.int); - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); + let name_a = state.fresh_name_str("a"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.int, + name_a.clone(), + ); + let name_b = state.fresh_name_str("b"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.string, + name_b.clone(), + ); let unification = state.fresh_unification_type(context); let Type::Unification(unification_id) = state.storage[unification] else { unreachable!("invariant violated"); }; - let bound_b = state.bound_variable(0, context.prim.int); - let bound_a = state.bound_variable(1, context.prim.string); + let bound_b = state.bound_variable(name_a, context.prim.int); + let bound_a = state.bound_variable(name_b, context.prim.string); let b_to_a = state.function(bound_b, bound_a); unification::solve(state, context, unification_id, b_to_a).unwrap(); @@ -123,7 +145,12 @@ fn test_solve_escaping_variable() { let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // [a :: Int] - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.int); + let name_a = state.fresh_name_str("a"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.int, + name_a, + ); // ?u created at depth C = 1 let unification = state.fresh_unification_type(context); @@ -132,10 +159,15 @@ fn test_solve_escaping_variable() { }; // [a :: Int, b :: String] S = 2 - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); + let name_b = state.fresh_name_str("b"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.string, + name_b.clone(), + ); // b is at level 1 which is C(1) <= level(1) < S(2) - let bound_b = state.bound_variable(1, context.prim.string); + let bound_b = state.bound_variable(name_b, context.prim.string); let solve_result = unification::solve(state, context, unification_id, bound_b).unwrap(); assert!(solve_result.is_none(), "should reject: b escapes the scope where ?u was created"); @@ -147,7 +179,12 @@ fn test_solve_promotion() { let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // [a :: Int] - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.int); + let name_a = state.fresh_name_str("a"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.int, + name_a, + ); let unification_a = state.fresh_unification_type(context); let Type::Unification(unification_id) = state.storage[unification_a] else { @@ -155,7 +192,12 @@ fn test_solve_promotion() { }; // [a :: Int, b :: String] - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.string); + let name_b = state.fresh_name_str("b"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.string, + name_b, + ); let unification_a_b = state.fresh_unification_type(context); unification::solve(state, context, unification_id, unification_a_b).unwrap(); @@ -213,10 +255,20 @@ fn test_quantify_ordering() { let (engine, id) = empty_engine(); let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.t); + let name_a = state.fresh_name_str("_"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.t, + name_a, + ); let unification_a = state.fresh_unification_type(context); - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.t); + let name_b = state.fresh_name_str("_"); + state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.t, + name_b, + ); let unification_b = state.fresh_unification_type(context); let function = state.storage.intern(Type::Function(unification_b, unification_a)); @@ -275,22 +327,24 @@ fn test_quantify_multiple_scoped() { fn make_forall_a_to_a(context: &CheckContext, state: &mut CheckState) -> TypeId { let fake_id = TypeVariableBindingId::new(FAKE_NONZERO_1); + let unbind_level = debruijn::Level(state.type_scope.size().0); - let level = state.type_scope.bind_forall(fake_id, context.prim.t); + let fresh = state.fresh_name_str("a"); + let name = state.type_scope.bind_forall(fake_id, context.prim.t, fresh); - let bound_a = state.bound_variable(0, context.prim.t); + let bound_a = state.bound_variable(name.clone(), context.prim.t); let a_to_a = state.function(bound_a, bound_a); let binder = ForallBinder { visible: false, implicit: false, - name: "a".into(), - level, + text: "a".into(), + variable: name, kind: context.prim.t, }; let forall_a_to_a = state.storage.intern(Type::Forall(binder, a_to_a)); - state.type_scope.unbind(level); + state.type_scope.unbind(unbind_level); forall_a_to_a } @@ -343,13 +397,22 @@ fn test_subtype_nested_forall() { let ContextState { ref context, ref mut state } = ContextState::new(&engine, id); // Create ∀a. ∀b. (a -> b -> a) - let level_a = - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_1), context.prim.t); - let level_b = - state.type_scope.bind_forall(TypeVariableBindingId::new(FAKE_NONZERO_2), context.prim.t); + let unbind_level = debruijn::Level(state.type_scope.size().0); + let fresh_a = state.fresh_name_str("a"); + let name_a = state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_1), + context.prim.t, + fresh_a, + ); + let fresh_b = state.fresh_name_str("b"); + let name_b = state.type_scope.bind_forall( + TypeVariableBindingId::new(FAKE_NONZERO_2), + context.prim.t, + fresh_b, + ); - let bound_a = state.bound_variable(1, context.prim.t); - let bound_b = state.bound_variable(0, context.prim.t); + let bound_a = state.bound_variable(name_a.clone(), context.prim.t); + let bound_b = state.bound_variable(name_b.clone(), context.prim.t); let b_to_a = state.function(bound_b, bound_a); let a_to_b_to_a = state.function(bound_a, b_to_a); @@ -357,25 +420,24 @@ fn test_subtype_nested_forall() { ForallBinder { visible: false, implicit: false, - name: "b".into(), - level: level_b, + text: "b".into(), + variable: name_b, kind: context.prim.t, }, a_to_b_to_a, )); - state.type_scope.unbind(level_b); let forall_a_b = state.storage.intern(Type::Forall( ForallBinder { visible: false, implicit: false, - name: "a".into(), - level: level_a, + text: "a".into(), + variable: name_a, kind: context.prim.t, }, forall_b, )); - state.type_scope.unbind(level_a); + state.type_scope.unbind(unbind_level); // ∀a. ∀b. (a -> b -> a) <: (Int -> String -> Int) should pass (LHS foralls get instantiated) let string_to_int = state.function(context.prim.string, context.prim.int); diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 760feb05..07a18d93 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -647,3 +647,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_316_synonym_derive_main() { run_test("316_synonym_derive", "Main"); } #[rustfmt::skip] #[test] fn test_317_higher_rank_fields_main() { run_test("317_higher_rank_fields", "Main"); } + +#[rustfmt::skip] #[test] fn test_318_higher_rank_newtype_main() { run_test("318_higher_rank_newtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_319_higher_rank_record_accessor_main() { run_test("319_higher_rank_record_accessor", "Main"); } + +#[rustfmt::skip] #[test] fn test_320_higher_rank_record_binder_main() { run_test("320_higher_rank_record_binder", "Main"); } + +#[rustfmt::skip] #[test] fn test_321_higher_rank_record_literal_main() { run_test("321_higher_rank_record_literal", "Main"); } diff --git a/tests-integration/tests/snapshots/checking__solve_bound.snap b/tests-integration/tests/snapshots/checking__solve_bound.snap index 8f4e11d6..c01301a9 100644 --- a/tests-integration/tests/snapshots/checking__solve_bound.snap +++ b/tests-integration/tests/snapshots/checking__solve_bound.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking.rs +assertion_line: 138 expression: snapshot --- -(&0 :: Int) -> (&1 :: String) :: Type +(a :: Int) -> (b :: String) :: Type From 250944de349dbe29f107aeb0d8a45979e290c061 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 01:47:20 +0800 Subject: [PATCH 143/386] Implement deferred generalisation for value groups This moves generalisation and globalisation for value signatures only after value equations are checked. This addresses an issue where kind variables that get solved in the equation body are generalised too early in the previous implementation. --- compiler-core/checking/src/algorithm.rs | 13 ++-- compiler-core/checking/src/algorithm/state.rs | 5 +- compiler-core/checking/src/algorithm/term.rs | 2 + .../checking/src/algorithm/term_item.rs | 39 ++++++++++- .../032_recursive_synonym_expansion/Main.snap | 30 +++++++++ .../322_phantom_kind_inference/Main.purs | 32 +++++++++ .../322_phantom_kind_inference/Main.snap | 66 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + ...g__recursive_synonym_expansion_errors.snap | 58 +++++++++++++++- 9 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 tests-integration/fixtures/checking/322_phantom_kind_inference/Main.purs create mode 100644 tests-integration/fixtures/checking/322_phantom_kind_inference/Main.snap diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 72ca605c..5258481d 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -368,10 +368,11 @@ where }; state.with_term_group(context, [*id], |state| { if let Some(item) = term_item::check_value_group(state, context, value_group)? { - term_item::commit_value_group(state, context, *id, item)?; + term_item::commit_inferred_value_group(state, context, *id, item)?; } Ok(()) })?; + term_item::commit_checked_value_group(state, context, *id)?; } Scc::Mutual(mutual) => { let value_groups = @@ -400,13 +401,17 @@ where } } for (item_id, group) in groups { - term_item::commit_value_group(state, context, item_id, group)?; + term_item::commit_inferred_value_group(state, context, item_id, group)?; } Ok(()) })?; - for value_group in with_signature { - term_item::check_value_group(state, context, value_group)?; + for value_group in &with_signature { + term_item::check_value_group(state, context, *value_group)?; + } + + for value_group in &with_signature { + term_item::commit_checked_value_group(state, context, value_group.item_id)?; } } } diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index b60a4a70..4b9ba599 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -319,6 +319,8 @@ pub struct CheckState { /// The in-progress binding group; used for recursive declarations. pub binding_group: BindingGroupContext, + pub equations: FxHashMap, + /// Error context breadcrumbs for [`CheckedModule::errors`]. pub check_steps: Vec, @@ -343,6 +345,7 @@ impl CheckState { implications: Default::default(), unification: Default::default(), binding_group: Default::default(), + equations: Default::default(), check_steps: Default::default(), defer_synonym_expansion: Default::default(), patterns: Default::default(), @@ -1000,7 +1003,7 @@ impl CheckState { F: FnOnce(&mut Self) -> T, { for item in group { - if !self.checked.terms.contains_key(&item) { + if !self.checked.terms.contains_key(&item) && !self.equations.contains_key(&item) { let t = self.fresh_unification_type(context); self.binding_group.terms.insert(item, t); } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 05841715..8b9ca2ee 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1468,6 +1468,8 @@ where let term_id = if file_id == context.id { if let Some(&k) = state.binding_group.terms.get(&term_id) { k + } else if let Some(&k) = state.equations.get(&term_id) { + k } else if let Some(&k) = state.checked.terms.get(&term_id) { transfer::localize(state, context, k) } else { diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index f5974cde..3a6e0f4b 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -47,7 +47,7 @@ where }; match item { - TermItemIr::Foreign { signature } | TermItemIr::ValueGroup { signature, .. } => { + TermItemIr::Foreign { signature } => { let Some(signature) = signature else { return Ok(()) }; let signature_variables = inspect::collect_signature_variables(context, *signature); @@ -65,6 +65,19 @@ where let global_type = transfer::globalize(state, context, quantified_type); state.checked.terms.insert(item_id, global_type); } + TermItemIr::ValueGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + + let signature_variables = inspect::collect_signature_variables(context, *signature); + state.surface_bindings.insert_term(item_id, signature_variables); + + let (inferred_type, _) = + kind::check_surface_kind(state, context, *signature, context.prim.t)?; + + crate::debug_fields!(state, context, { inferred_type = inferred_type }); + + state.equations.insert(item_id, inferred_type); + } TermItemIr::Operator { resolution, .. } => { let Some((file_id, term_id)) = *resolution else { return Ok(()) }; let id = term::lookup_file_term(state, context, file_id, term_id)?; @@ -310,8 +323,30 @@ where } } +pub fn commit_checked_value_group( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(inferred_type) = state.equations.remove(&item_id) else { + return Ok(()); + }; + + let quantified_type = quantify::quantify(state, inferred_type) + .map(|(quantified_type, _)| quantified_type) + .unwrap_or(inferred_type); + + let global_type = transfer::globalize(state, context, quantified_type); + state.checked.terms.insert(item_id, global_type); + + Ok(()) +} + /// Generalises an [`InferredValueGroup`]. -pub fn commit_value_group( +pub fn commit_inferred_value_group( state: &mut CheckState, context: &CheckContext, item_id: TermItemId, diff --git a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap index 7ac4b8b4..9cac43c7 100644 --- a/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking/032_recursive_synonym_expansion/Main.snap @@ -68,3 +68,33 @@ error[RecursiveSynonymExpansion]: Recursive type synonym expansion | 14 | testH :: H -> H | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 9:11..9:12 + | +9 | testF x = x + | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 9:11..9:12 + | +9 | testF x = x + | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 12:11..12:12 + | +12 | testG x = x + | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 12:11..12:12 + | +12 | testG x = x + | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 15:11..15:12 + | +15 | testH x = x + | ^ +error[RecursiveSynonymExpansion]: Recursive type synonym expansion + --> 15:11..15:12 + | +15 | testH x = x + | ^ diff --git a/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.purs b/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.purs new file mode 100644 index 00000000..d08c4039 --- /dev/null +++ b/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.purs @@ -0,0 +1,32 @@ +module Main where + +foreign import unsafeCoerce :: forall a b. a -> b + +data Query input a = Query input a +data HM state m a = HM state (m a) +data Solid input m + +type Spec state input m = + { eval :: forall a. Query input a -> HM state m a + } + +mkSpec + :: forall state input m + . Spec state input m + -> Solid input m +mkSpec = unsafeCoerce + +unSpec + :: forall input m a + . (forall state. Spec state input m -> a) + -> Solid input m + -> a +unSpec = unsafeCoerce + +hoist + :: forall input m + . Solid input m + -> Solid input m +hoist = unSpec \c -> mkSpec + { eval: c.eval + } diff --git a/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.snap b/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.snap new file mode 100644 index 00000000..590cebd8 --- /dev/null +++ b/tests-integration/fixtures/checking/322_phantom_kind_inference/Main.snap @@ -0,0 +1,66 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +Query :: + forall (input :: Type) (a :: Type). + (input :: Type) -> (a :: Type) -> Query (input :: Type) (a :: Type) +HM :: + forall (t7 :: Type) (state :: Type) (m :: (t7 :: Type) -> Type) (a :: (t7 :: Type)). + (state :: Type) -> + (m :: (t7 :: Type) -> Type) (a :: (t7 :: Type)) -> + HM @(t7 :: Type) (state :: Type) (m :: (t7 :: Type) -> Type) (a :: (t7 :: Type)) +mkSpec :: + forall (state :: Type) (input :: Type) (m :: Type -> Type). + Spec (state :: Type) (input :: Type) (m :: Type -> Type) -> + Solid @Type @(Type -> Type) (input :: Type) (m :: Type -> Type) +unSpec :: + forall (input :: Type) (m :: Type -> Type) (a :: Type). + (forall (state :: Type). + Spec (state :: Type) (input :: Type) (m :: Type -> Type) -> (a :: Type)) -> + Solid @Type @(Type -> Type) (input :: Type) (m :: Type -> Type) -> + (a :: Type) +hoist :: + forall (input :: Type) (m :: Type -> Type). + Solid @Type @(Type -> Type) (input :: Type) (m :: Type -> Type) -> + Solid @Type @(Type -> Type) (input :: Type) (m :: Type -> Type) + +Types +Query :: Type -> Type -> Type +HM :: forall (t7 :: Type). Type -> ((t7 :: Type) -> Type) -> (t7 :: Type) -> Type +Solid :: forall (t9 :: Type) (t10 :: Type). (t9 :: Type) -> (t10 :: Type) -> Type +Spec :: Type -> Type -> (Type -> Type) -> Type + +Synonyms +Spec = forall (state :: Type) (input :: Type) (m :: Type -> Type). + { eval :: + forall (a :: Type). + Query (input :: Type) (a :: Type) -> + HM @Type (state :: Type) (m :: Type -> Type) (a :: Type) + } + Quantified = :0 + Kind = :0 + Type = :3 + + +Data +Query + Quantified = :0 + Kind = :0 + +HM + Quantified = :1 + Kind = :0 + +Solid + Quantified = :2 + Kind = :0 + + +Roles +Query = [Representational, Representational] +HM = [Representational, Representational, Nominal] +Solid = [Phantom, Phantom] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 07a18d93..8e183939 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -655,3 +655,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_320_higher_rank_record_binder_main() { run_test("320_higher_rank_record_binder", "Main"); } #[rustfmt::skip] #[test] fn test_321_higher_rank_record_literal_main() { run_test("321_higher_rank_record_literal", "Main"); } + +#[rustfmt::skip] #[test] fn test_322_phantom_kind_inference_main() { run_test("322_phantom_kind_inference", "Main"); } diff --git a/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap b/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap index 3319db7d..a28dda92 100644 --- a/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap +++ b/tests-integration/tests/snapshots/checking__recursive_synonym_expansion_errors.snap @@ -1,6 +1,6 @@ --- source: tests-integration/tests/checking.rs -assertion_line: 974 +assertion_line: 1050 expression: checked.errors --- [ @@ -72,4 +72,60 @@ expression: checked.errors ), ], }, + CheckError { + kind: RecursiveSynonymExpansion { + file_id: Idx::(9), + item_id: Idx::(0), + }, + step: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(20), + ), + ], + }, + CheckError { + kind: RecursiveSynonymExpansion { + file_id: Idx::(9), + item_id: Idx::(0), + }, + step: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(20), + ), + ], + }, + CheckError { + kind: RecursiveSynonymExpansion { + file_id: Idx::(9), + item_id: Idx::(2), + }, + step: [ + TermDeclaration( + Idx::(1), + ), + CheckingExpression( + AstId(30), + ), + ], + }, + CheckError { + kind: RecursiveSynonymExpansion { + file_id: Idx::(9), + item_id: Idx::(2), + }, + step: [ + TermDeclaration( + Idx::(1), + ), + CheckingExpression( + AstId(30), + ), + ], + }, ] From e45f7d0427a6dfa34017aaef96009dbe5b871f8a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 01:59:52 +0800 Subject: [PATCH 144/386] Add failing test case for operator generalisation This demonstrates a current limitation in the type checker where operators poiinting to kind-polymorphic value groups infer to an incorrect type. This issue can be addressed by deferring generalisation for operator declarations, much like it is now for value declarations. --- .../Main.purs | 16 +++++++++++ .../Main.snap | 27 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 3 files changed, 45 insertions(+) create mode 100644 tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.purs create mode 100644 tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap diff --git a/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.purs b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.purs new file mode 100644 index 00000000..54e17173 --- /dev/null +++ b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.purs @@ -0,0 +1,16 @@ +module Main where + +foreign import data Component :: forall k. k -> Type + +data ForceTypeType :: (Type -> Type) -> Type +data ForceTypeType f = ForceTypeType + +knownInEquation :: forall m. Component m -> Component m -> Int +knownInEquation _ _ = + let + forced :: ForceTypeType m + forced = ForceTypeType + in + 42 + +infix 4 knownInEquation as +++ diff --git a/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap new file mode 100644 index 00000000..2abae556 --- /dev/null +++ b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +ForceTypeType :: forall (f :: Type -> Type). ForceTypeType (f :: Type -> Type) +knownInEquation :: + forall (m :: Type -> Type). + Component @(Type -> Type) (m :: Type -> Type) -> + Component @(Type -> Type) (m :: Type -> Type) -> + Int ++++ :: forall (m :: ???). Component @??? (m :: ???) -> Component @??? (m :: ???) -> Int + +Types +Component :: forall (k :: Type). (k :: Type) -> Type +ForceTypeType :: (Type -> Type) -> Type + +Data +ForceTypeType + Quantified = :0 + Kind = :0 + + +Roles +Component = [Nominal] +ForceTypeType = [Phantom] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 8e183939..c2b2e699 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -657,3 +657,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_321_higher_rank_record_literal_main() { run_test("321_higher_rank_record_literal", "Main"); } #[rustfmt::skip] #[test] fn test_322_phantom_kind_inference_main() { run_test("322_phantom_kind_inference", "Main"); } + +#[rustfmt::skip] #[test] fn test_323_operator_deferred_generalise_main() { run_test("323_operator_deferred_generalise", "Main"); } From dbd55a5747a5493c52cedbe916f7c9cc30a800f7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 02:07:59 +0800 Subject: [PATCH 145/386] Implement deferred generalisation for operators --- compiler-core/checking/src/algorithm.rs | 2 ++ .../checking/src/algorithm/quantify.rs | 2 +- compiler-core/checking/src/algorithm/state.rs | 23 +++++++++++++-- compiler-core/checking/src/algorithm/term.rs | 4 +-- .../checking/src/algorithm/term_item.rs | 28 +++++++++++++------ .../Main.snap | 6 +++- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 5258481d..8910eb73 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -94,6 +94,8 @@ pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryRes check_instance_members(&mut state, &context)?; check_derive_members(&mut state, &context, &derive_results)?; + term_item::commit_pending_terms(&mut state, &context); + Ok(state.checked) } diff --git a/compiler-core/checking/src/algorithm/quantify.rs b/compiler-core/checking/src/algorithm/quantify.rs index 59e967c9..5c2a18f4 100644 --- a/compiler-core/checking/src/algorithm/quantify.rs +++ b/compiler-core/checking/src/algorithm/quantify.rs @@ -319,7 +319,7 @@ pub fn quantify_instance(state: &mut CheckState, instance: &mut Instance) -> Opt let kind_variables = substitutions.values().cloned(); let kind_variables = kind_variables.sorted_by_key(|(name, _)| name.unique); - let kind_variables = kind_variables.map(|(name, kind)| (name, kind)).collect_vec(); + let kind_variables = kind_variables.collect_vec(); let arguments = instance.arguments.iter().map(|&(t, k)| { let t = SubstituteUnification::on(&substitutions, state, t); diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 4b9ba599..9b32678e 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -294,6 +294,21 @@ impl SurfaceBindings { } } +#[derive(Clone, Copy)] +pub enum PendingTermType { + Immediate(TypeId), + Deferred(TypeId), +} + +impl From for TypeId { + fn from(value: PendingTermType) -> Self { + match value { + PendingTermType::Immediate(id) => id, + PendingTermType::Deferred(id) => id, + } + } +} + /// The core state structure threaded through the [`algorithm`]. /// /// [`algorithm`]: crate::algorithm @@ -319,7 +334,9 @@ pub struct CheckState { /// The in-progress binding group; used for recursive declarations. pub binding_group: BindingGroupContext, - pub equations: FxHashMap, + /// Stores terms whose signatures have been kind-checked that still need + /// additional unification before moving into [`CheckedModule::terms`]. + pub pending_terms: FxHashMap, /// Error context breadcrumbs for [`CheckedModule::errors`]. pub check_steps: Vec, @@ -345,7 +362,7 @@ impl CheckState { implications: Default::default(), unification: Default::default(), binding_group: Default::default(), - equations: Default::default(), + pending_terms: Default::default(), check_steps: Default::default(), defer_synonym_expansion: Default::default(), patterns: Default::default(), @@ -1003,7 +1020,7 @@ impl CheckState { F: FnOnce(&mut Self) -> T, { for item in group { - if !self.checked.terms.contains_key(&item) && !self.equations.contains_key(&item) { + if !self.checked.terms.contains_key(&item) && !self.pending_terms.contains_key(&item) { let t = self.fresh_unification_type(context); self.binding_group.terms.insert(item, t); } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 8b9ca2ee..1e844367 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -1468,8 +1468,8 @@ where let term_id = if file_id == context.id { if let Some(&k) = state.binding_group.terms.get(&term_id) { k - } else if let Some(&k) = state.equations.get(&term_id) { - k + } else if let Some(&k) = state.pending_terms.get(&term_id) { + k.into() } else if let Some(&k) = state.checked.terms.get(&term_id) { transfer::localize(state, context, k) } else { diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 3a6e0f4b..0da5420c 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -8,7 +8,7 @@ use lowering::TermItemIr; use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; -use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding}; +use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding, PendingTermType}; use crate::algorithm::{ constraint, equation, inspect, kind, normalise, quantify, substitute, term, transfer, unification, @@ -26,7 +26,7 @@ pub struct InferredValueGroup { /// /// This function checks the term signatures for [`TermItemIr::Foreign`], /// [`TermItemIr::ValueGroup`], and [`TermItemIr::Operator`], inserting -/// them into [`CheckState::checked`] upon completion. +/// them into [`CheckState::pending_terms`] as [`PendingTermType`] entries. /// /// For [`TermItemIr::ValueGroup`] specifically, it also invokes the /// [`inspect::collect_signature_variables`] function to collect type @@ -62,8 +62,7 @@ where crate::debug_fields!(state, context, { quantified_type = quantified_type }); - let global_type = transfer::globalize(state, context, quantified_type); - state.checked.terms.insert(item_id, global_type); + state.pending_terms.insert(item_id, PendingTermType::Immediate(quantified_type)); } TermItemIr::ValueGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; @@ -76,14 +75,13 @@ where crate::debug_fields!(state, context, { inferred_type = inferred_type }); - state.equations.insert(item_id, inferred_type); + state.pending_terms.insert(item_id, PendingTermType::Deferred(inferred_type)); } TermItemIr::Operator { resolution, .. } => { let Some((file_id, term_id)) = *resolution else { return Ok(()) }; let id = term::lookup_file_term(state, context, file_id, term_id)?; - let global_type = transfer::globalize(state, context, id); - state.checked.terms.insert(item_id, global_type); + state.pending_terms.insert(item_id, PendingTermType::Deferred(id)); } _ => (), } @@ -331,7 +329,8 @@ pub fn commit_checked_value_group( where Q: ExternalQueries, { - let Some(inferred_type) = state.equations.remove(&item_id) else { + let Some(PendingTermType::Deferred(inferred_type)) = state.pending_terms.remove(&item_id) + else { return Ok(()); }; @@ -345,6 +344,19 @@ where Ok(()) } +/// Commits remaining pending term entries (foreign/operator) from +/// [`CheckState::pending_terms`] into [`CheckedModule::terms`]. +pub fn commit_pending_terms(state: &mut CheckState, context: &CheckContext) +where + Q: ExternalQueries, +{ + for (item_id, pending_type) in state.pending_terms.drain().collect_vec() { + let local_type = pending_type.into(); + let global_type = transfer::globalize(state, context, local_type); + state.checked.terms.insert(item_id, global_type); + } +} + /// Generalises an [`InferredValueGroup`]. pub fn commit_inferred_value_group( state: &mut CheckState, diff --git a/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap index 2abae556..3b71782b 100644 --- a/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap +++ b/tests-integration/fixtures/checking/323_operator_deferred_generalise/Main.snap @@ -10,7 +10,11 @@ knownInEquation :: Component @(Type -> Type) (m :: Type -> Type) -> Component @(Type -> Type) (m :: Type -> Type) -> Int -+++ :: forall (m :: ???). Component @??? (m :: ???) -> Component @??? (m :: ???) -> Int ++++ :: + forall (m :: Type -> Type). + Component @(Type -> Type) (m :: Type -> Type) -> + Component @(Type -> Type) (m :: Type -> Type) -> + Int Types Component :: forall (k :: Type). (k :: Type) -> Type From d20f315ac67cdfba6ff154ad2244721784113bbc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 03:05:46 +0800 Subject: [PATCH 146/386] Fix missing generalisation for Deferred --- compiler-core/checking/src/algorithm/term_item.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 0da5420c..23322efd 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -351,7 +351,12 @@ where Q: ExternalQueries, { for (item_id, pending_type) in state.pending_terms.drain().collect_vec() { - let local_type = pending_type.into(); + let local_type = match pending_type { + PendingTermType::Immediate(id) => id, + PendingTermType::Deferred(id) => { + quantify::quantify(state, id).map(|(id, _)| id).unwrap_or(id) + } + }; let global_type = transfer::globalize(state, context, local_type); state.checked.terms.insert(item_id, global_type); } From 22953f319d9631dff9c3855b447755a773fe7c87 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 03:38:07 +0800 Subject: [PATCH 147/386] Add test case for immediate generalisation on foreign --- .../324_foreign_kind_polymorphism/Main.purs | 11 ++++++++ .../324_foreign_kind_polymorphism/Main.snap | 26 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 3 files changed, 39 insertions(+) create mode 100644 tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.purs create mode 100644 tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.snap diff --git a/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.purs b/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.purs new file mode 100644 index 00000000..be5caf0b --- /dev/null +++ b/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.purs @@ -0,0 +1,11 @@ +module Main where + +data Identity :: forall k. (k -> k) -> Type +data Identity a = Identity + +foreign import fn :: forall k. Identity k -> Identity k + +-- Foreign values must be generalised immediately +-- to avoid solving them to concrete types on usage. + +test = fn (Identity :: Identity Array) diff --git a/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.snap b/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.snap new file mode 100644 index 00000000..8c0613fc --- /dev/null +++ b/tests-integration/fixtures/checking/324_foreign_kind_polymorphism/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type)). + Identity @(k :: Type) (a :: (k :: Type) -> (k :: Type)) +fn :: + forall (t3 :: Type) (k :: (t3 :: Type) -> (t3 :: Type)). + Identity @(t3 :: Type) (k :: (t3 :: Type) -> (t3 :: Type)) -> + Identity @(t3 :: Type) (k :: (t3 :: Type) -> (t3 :: Type)) +test :: Identity @Type Array + +Types +Identity :: forall (k :: Type). ((k :: Type) -> (k :: Type)) -> Type + +Data +Identity + Quantified = :0 + Kind = :1 + + +Roles +Identity = [Phantom] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index c2b2e699..29b162a7 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -659,3 +659,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_322_phantom_kind_inference_main() { run_test("322_phantom_kind_inference", "Main"); } #[rustfmt::skip] #[test] fn test_323_operator_deferred_generalise_main() { run_test("323_operator_deferred_generalise", "Main"); } + +#[rustfmt::skip] #[test] fn test_324_foreign_kind_polymorphism_main() { run_test("324_foreign_kind_polymorphism", "Main"); } From f4047d3e8139f284b37832dc8acd1ab65eeda37f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 03:38:42 +0800 Subject: [PATCH 148/386] Small observability fixes --- compiler-core/checking/src/algorithm/term_item.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 23322efd..ce8a49ef 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -61,7 +61,6 @@ where .unwrap_or(inferred_type); crate::debug_fields!(state, context, { quantified_type = quantified_type }); - state.pending_terms.insert(item_id, PendingTermType::Immediate(quantified_type)); } TermItemIr::ValueGroup { signature, .. } => { @@ -74,14 +73,14 @@ where kind::check_surface_kind(state, context, *signature, context.prim.t)?; crate::debug_fields!(state, context, { inferred_type = inferred_type }); - state.pending_terms.insert(item_id, PendingTermType::Deferred(inferred_type)); } TermItemIr::Operator { resolution, .. } => { let Some((file_id, term_id)) = *resolution else { return Ok(()) }; - let id = term::lookup_file_term(state, context, file_id, term_id)?; + let inferred_type = term::lookup_file_term(state, context, file_id, term_id)?; - state.pending_terms.insert(item_id, PendingTermType::Deferred(id)); + crate::debug_fields!(state, context, { inferred_type = inferred_type }); + state.pending_terms.insert(item_id, PendingTermType::Deferred(inferred_type)); } _ => (), } From 46f7f93e178b8f456a0891f3987c9d4606470b84 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 13:23:42 +0800 Subject: [PATCH 149/386] Implement deferred kind generalisation --- compiler-core/checking/src/algorithm.rs | 1 + compiler-core/checking/src/algorithm/kind.rs | 2 + compiler-core/checking/src/algorithm/state.rs | 26 +++++-- .../checking/src/algorithm/term_item.rs | 19 +++-- .../checking/src/algorithm/type_item.rs | 78 +++++++++++++++++-- .../Main.purs | 11 +++ .../Main.snap | 32 ++++++++ tests-integration/tests/checking/generated.rs | 2 + 8 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.purs create mode 100644 tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.snap diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 8910eb73..5a646f12 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -86,6 +86,7 @@ pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryRes check_type_signatures(&mut state, &context)?; check_type_definitions(&mut state, &context)?; + type_item::commit_pending_types(&mut state, &context); check_term_signatures(&mut state, &context)?; check_instance_heads(&mut state, &context)?; diff --git a/compiler-core/checking/src/algorithm/kind.rs b/compiler-core/checking/src/algorithm/kind.rs index 3f295770..33b31928 100644 --- a/compiler-core/checking/src/algorithm/kind.rs +++ b/compiler-core/checking/src/algorithm/kind.rs @@ -659,6 +659,8 @@ where let type_id = if file_id == context.id { if let Some(k) = state.binding_group.lookup_type(type_id) { k + } else if let Some(&k) = state.pending_types.get(&type_id) { + TypeId::from(k) } else if let Some(&k) = state.checked.types.get(&type_id) { transfer::localize(state, context, k) } else { diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index 9b32678e..e181b56e 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -295,16 +295,16 @@ impl SurfaceBindings { } #[derive(Clone, Copy)] -pub enum PendingTermType { +pub enum PendingType { Immediate(TypeId), Deferred(TypeId), } -impl From for TypeId { - fn from(value: PendingTermType) -> Self { +impl From for TypeId { + fn from(value: PendingType) -> Self { match value { - PendingTermType::Immediate(id) => id, - PendingTermType::Deferred(id) => id, + PendingType::Immediate(id) => id, + PendingType::Deferred(id) => id, } } } @@ -336,7 +336,11 @@ pub struct CheckState { /// Stores terms whose signatures have been kind-checked that still need /// additional unification before moving into [`CheckedModule::terms`]. - pub pending_terms: FxHashMap, + pub pending_terms: FxHashMap, + + /// Stores types whose signatures have been kind-checked that still need + /// additional unification before moving into [`CheckedModule::types`]. + pub pending_types: FxHashMap, /// Error context breadcrumbs for [`CheckedModule::errors`]. pub check_steps: Vec, @@ -363,6 +367,7 @@ impl CheckState { unification: Default::default(), binding_group: Default::default(), pending_terms: Default::default(), + pending_types: Default::default(), check_steps: Default::default(), defer_synonym_expansion: Default::default(), patterns: Default::default(), @@ -1052,6 +1057,9 @@ impl CheckState { if self.checked.types.contains_key(&item_id) { return false; } + if self.pending_types.contains_key(&item_id) { + return false; + } true }); @@ -1077,8 +1085,10 @@ impl CheckState { ); let kind = self.binding_group.lookup_type(item_id).or_else(|| { - let kind = self.checked.types.get(&item_id)?; - Some(transfer::localize(self, context, *kind)) + self.pending_types.get(&item_id).map(|&k| TypeId::from(k)).or_else(|| { + let kind = self.checked.types.get(&item_id)?; + Some(transfer::localize(self, context, *kind)) + }) }); let kind = kind.expect("invariant violated: expected kind for operator target"); diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index ce8a49ef..c6e62e1f 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -8,7 +8,7 @@ use lowering::TermItemIr; use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; -use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding, PendingTermType}; +use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding, PendingType}; use crate::algorithm::{ constraint, equation, inspect, kind, normalise, quantify, substitute, term, transfer, unification, @@ -61,7 +61,7 @@ where .unwrap_or(inferred_type); crate::debug_fields!(state, context, { quantified_type = quantified_type }); - state.pending_terms.insert(item_id, PendingTermType::Immediate(quantified_type)); + state.pending_terms.insert(item_id, PendingType::Immediate(quantified_type)); } TermItemIr::ValueGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; @@ -73,14 +73,14 @@ where kind::check_surface_kind(state, context, *signature, context.prim.t)?; crate::debug_fields!(state, context, { inferred_type = inferred_type }); - state.pending_terms.insert(item_id, PendingTermType::Deferred(inferred_type)); + state.pending_terms.insert(item_id, PendingType::Deferred(inferred_type)); } TermItemIr::Operator { resolution, .. } => { let Some((file_id, term_id)) = *resolution else { return Ok(()) }; let inferred_type = term::lookup_file_term(state, context, file_id, term_id)?; crate::debug_fields!(state, context, { inferred_type = inferred_type }); - state.pending_terms.insert(item_id, PendingTermType::Deferred(inferred_type)); + state.pending_terms.insert(item_id, PendingType::Deferred(inferred_type)); } _ => (), } @@ -328,8 +328,7 @@ pub fn commit_checked_value_group( where Q: ExternalQueries, { - let Some(PendingTermType::Deferred(inferred_type)) = state.pending_terms.remove(&item_id) - else { + let Some(PendingType::Deferred(inferred_type)) = state.pending_terms.remove(&item_id) else { return Ok(()); }; @@ -343,16 +342,16 @@ where Ok(()) } -/// Commits remaining pending term entries (foreign/operator) from -/// [`CheckState::pending_terms`] into [`CheckedModule::terms`]. +/// Commits remaining pending term entries from [`CheckState::pending_terms`] +/// into [`CheckedModule::terms`]. pub fn commit_pending_terms(state: &mut CheckState, context: &CheckContext) where Q: ExternalQueries, { for (item_id, pending_type) in state.pending_terms.drain().collect_vec() { let local_type = match pending_type { - PendingTermType::Immediate(id) => id, - PendingTermType::Deferred(id) => { + PendingType::Immediate(id) => id, + PendingType::Deferred(id) => { quantify::quantify(state, id).map(|(id, _)| id).unwrap_or(id) } }; diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 81afa86c..1310ac92 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -10,7 +10,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; -use crate::algorithm::state::{CheckContext, CheckState, CheckedConstructor}; +use crate::algorithm::state::{CheckContext, CheckState, CheckedConstructor, PendingType}; use crate::algorithm::{inspect, kind, quantify, transfer, unification}; use crate::core::{ Class, DataLike, ForallBinder, Operator, Role, Synonym, Type, TypeId, Variable, debruijn, @@ -399,6 +399,14 @@ where { let type_id = transfer::globalize(state, context, quantified_type); state.checked.types.insert(item_id, type_id); + } else if inferred_kind.is_none() + && let Some(PendingType::Deferred(deferred_kind)) = state.pending_types.remove(&item_id) + { + let quantified_type = quantify::quantify(state, deferred_kind) + .map(|(quantified_type, _)| quantified_type) + .unwrap_or(deferred_kind); + let type_id = transfer::globalize(state, context, quantified_type); + state.checked.types.insert(item_id, type_id); } let synonym_type = type_variables.iter().rfold(synonym_type, |inner, binder| { @@ -461,6 +469,23 @@ where let data_like = DataLike { quantified_variables, kind_variables }; state.checked.data.insert(item_id, data_like); + let type_id = transfer::globalize(state, context, quantified_type); + state.checked.types.insert(item_id, type_id); + } else if let Some(PendingType::Deferred(deferred_kind)) = + state.pending_types.remove(&item_id) + { + let (quantified_type, quantified_variables) = + if let Some(result) = quantify::quantify(state, deferred_kind) { + result + } else { + (deferred_kind, debruijn::Size(0)) + }; + + let kind_variables = debruijn::Size(kind_variable_count); + + let data_like = DataLike { quantified_variables, kind_variables }; + state.checked.data.insert(item_id, data_like); + let type_id = transfer::globalize(state, context, quantified_type); state.checked.types.insert(item_id, type_id); } else { @@ -554,6 +579,13 @@ where { let type_id = transfer::globalize(state, context, quantified_type); state.checked.types.insert(item_id, type_id); + } else if inferred_kind.is_none() + && let Some(PendingType::Deferred(deferred_kind)) = state.pending_types.remove(&item_id) + { + let quantified_type = + quantify::quantify(state, deferred_kind).map(|(qt, _)| qt).unwrap_or(deferred_kind); + let type_id = transfer::globalize(state, context, quantified_type); + state.checked.types.insert(item_id, type_id); } let mut class = { @@ -628,6 +660,24 @@ where Ok(()) } +/// Commits remaining pending type entries from [`CheckState::pending_types`] +/// into [`CheckedModule::types`]. +pub fn commit_pending_types(state: &mut CheckState, context: &CheckContext) +where + Q: ExternalQueries, +{ + for (item_id, pending_kind) in state.pending_types.drain().collect_vec() { + let local_type = match pending_kind { + PendingType::Immediate(id) => id, + PendingType::Deferred(id) => { + quantify::quantify(state, id).map(|(id, _)| id).unwrap_or(id) + } + }; + let global_type = transfer::globalize(state, context, local_type); + state.checked.types.insert(item_id, global_type); + } +} + /// Checks the kind signature of a type item. /// /// This function also generalises the type and inserts it directly to @@ -653,11 +703,28 @@ where }; match item { + TypeItemIr::Foreign { signature, .. } => { + let Some(signature) = signature else { + return Ok(()); + }; + + let signature_variables = inspect::collect_signature_variables(context, *signature); + state.surface_bindings.insert_type(item_id, signature_variables); + + let (inferred_type, _) = + kind::check_surface_kind(state, context, *signature, context.prim.t)?; + + let quantified_type = quantify::quantify(state, inferred_type) + .map(|(quantified_type, _)| quantified_type) + .unwrap_or(inferred_type); + + state.pending_types.insert(item_id, PendingType::Immediate(quantified_type)); + } + TypeItemIr::DataGroup { signature, .. } | TypeItemIr::NewtypeGroup { signature, .. } | TypeItemIr::SynonymGroup { signature, .. } - | TypeItemIr::ClassGroup { signature, .. } - | TypeItemIr::Foreign { signature, .. } => { + | TypeItemIr::ClassGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()); }; @@ -668,10 +735,7 @@ where let (inferred_type, _) = kind::check_surface_kind(state, context, *signature, context.prim.t)?; - if let Some((quantified_type, _)) = quantify::quantify(state, inferred_type) { - let type_id = transfer::globalize(state, context, quantified_type); - state.checked.types.insert(item_id, type_id); - } + state.pending_types.insert(item_id, PendingType::Deferred(inferred_type)); } TypeItemIr::Operator { .. } => {} diff --git a/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.purs b/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.purs new file mode 100644 index 00000000..f7308691 --- /dev/null +++ b/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.purs @@ -0,0 +1,11 @@ +module Main where + +foreign import data Component :: forall k. k -> Type + +data ForceTypeType :: (Type -> Type) -> Type +data ForceTypeType f = ForceTypeType + +data Known :: forall k. Component k -> Type -> Type +data Known a b = Known (ForceTypeType k) + +infix 4 type Known as +++ diff --git a/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.snap b/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.snap new file mode 100644 index 00000000..aff7fe0f --- /dev/null +++ b/tests-integration/fixtures/checking/325_type_kind_deferred_generalise/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +ForceTypeType :: forall (f :: Type -> Type). ForceTypeType (f :: Type -> Type) +Known :: + forall (k :: Type -> Type) (a :: Component @(Type -> Type) (k :: Type -> Type)) (b :: Type). + ForceTypeType (k :: Type -> Type) -> + Known @(k :: Type -> Type) (a :: Component @(Type -> Type) (k :: Type -> Type)) (b :: Type) + +Types +Component :: forall (k :: Type). (k :: Type) -> Type +ForceTypeType :: (Type -> Type) -> Type +Known :: forall (k :: Type -> Type). Component @(Type -> Type) (k :: Type -> Type) -> Type -> Type ++++ :: forall (k :: Type -> Type). Component @(Type -> Type) (k :: Type -> Type) -> Type -> Type + +Data +ForceTypeType + Quantified = :0 + Kind = :0 + +Known + Quantified = :0 + Kind = :1 + + +Roles +Component = [Nominal] +ForceTypeType = [Phantom] +Known = [Phantom, Phantom] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 29b162a7..65600a9b 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -661,3 +661,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_323_operator_deferred_generalise_main() { run_test("323_operator_deferred_generalise", "Main"); } #[rustfmt::skip] #[test] fn test_324_foreign_kind_polymorphism_main() { run_test("324_foreign_kind_polymorphism", "Main"); } + +#[rustfmt::skip] #[test] fn test_325_type_kind_deferred_generalise_main() { run_test("325_type_kind_deferred_generalise", "Main"); } From a2f8e1dc0bf497cb1a35ec49e5699e3bc0e7f16f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 15:19:50 +0800 Subject: [PATCH 150/386] Fix formatting --- compiler-core/checking/src/algorithm/type_item.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 1310ac92..56240b38 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -471,8 +471,7 @@ where let type_id = transfer::globalize(state, context, quantified_type); state.checked.types.insert(item_id, type_id); - } else if let Some(PendingType::Deferred(deferred_kind)) = - state.pending_types.remove(&item_id) + } else if let Some(PendingType::Deferred(deferred_kind)) = state.pending_types.remove(&item_id) { let (quantified_type, quantified_variables) = if let Some(result) = quantify::quantify(state, deferred_kind) { From 33940e31f61da7ce30d149025edf261c3ea1c5bd Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 22:36:12 +0800 Subject: [PATCH 151/386] Implement initial syntax-driven checking --- compiler-core/checking/src/algorithm/term.rs | 426 ++++++++++++++---- .../checking/043_binder_named/Main.snap | 7 +- .../checking/060_array_binder/Main.snap | 4 +- .../fixtures/checking/062_case_of/Main.snap | 3 +- .../068_expression_sections/Main.snap | 15 +- .../Main.snap | 5 +- .../070_record_access_sections/Main.snap | 70 +-- .../071_record_update_sections/Main.snap | 37 +- .../checking/102_builtin_row/Main.snap | 18 +- .../Main.snap | 10 + .../Main.snap | 5 + .../273_class_member_instantiation/Main.snap | 14 +- .../Main.snap | 4 +- 13 files changed, 455 insertions(+), 163 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 1e844367..89881573 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -6,7 +6,6 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::unification::ElaborationMode; use crate::algorithm::{ binder, equation, exhaustiveness, inspect, kind, normalise, operator, substitute, toolkit, transfer, unification, @@ -19,82 +18,118 @@ use crate::error::{ErrorKind, ErrorStep}; pub fn check_expression( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, expected: TypeId, ) -> QueryResult where Q: ExternalQueries, { - crate::trace_fields!(state, context, { expected = expected }); - state.with_error_step(ErrorStep::CheckingExpression(expr_id), |state| { - let inferred = infer_expression_quiet(state, context, expr_id)?; - let _ = unification::subtype(state, context, inferred, expected)?; - crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); - Ok(inferred) + state.with_error_step(ErrorStep::CheckingExpression(expression), |state| { + crate::trace_fields!(state, context, { expected = expected }); + check_expression_quiet(state, context, expression, expected) }) } -fn check_expression_argument( +fn check_expression_quiet( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, expected: TypeId, ) -> QueryResult where Q: ExternalQueries, { - state.with_error_step(ErrorStep::CheckingExpression(expr_id), |state| { - let inferred = infer_expression_quiet(state, context, expr_id)?; - // Instantiate the inferred type when the expected type is not - // polymorphic, so constraints are collected as wanted rather - // than leaking into unification. Skipped for higher-rank - // arguments where constraints must match structurally. - let expected = state.normalize_type(expected); - if matches!(state.storage[expected], Type::Constrained(..)) { - // Peel constraints from expected as givens so they can - // discharge wanted constraints from the inferred type - // e.g. unsafePartial discharging Partial - let expected = toolkit::collect_given_constraints(state, expected); - let inferred = toolkit::instantiate_constrained(state, inferred); - unification::subtype_with_mode( - state, - context, - inferred, - expected, - ElaborationMode::No, - )?; - crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); - Ok(inferred) - } else if matches!(state.storage[expected], Type::Forall(..)) { - // Higher-rank expected type. Skolemise forall and collect - // given constraints on the expected side, then instantiate and - // collect wanted constraints on the inferred side, before - // comparing the unwrapped body types. - let expected = toolkit::skolemise_forall(state, expected); - let expected = toolkit::collect_given_constraints(state, expected); - let inferred = toolkit::instantiate_constrained(state, inferred); - unification::subtype_with_mode( - state, - context, - inferred, - expected, - ElaborationMode::No, - )?; - crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); - Ok(inferred) + // TODO: move normalisation to toolkit + let expected = normalise::normalise_expand_type(state, context, expected)?; + let expected = toolkit::skolemise_forall(state, expected); + let expected = toolkit::collect_given_constraints(state, expected); + if let Some(section_result) = context.sectioned.expressions.get(&expression) { + check_sectioned_expression(state, context, expression, section_result, expected) + } else { + check_expression_core(state, context, expression, expected) + } +} + +fn check_sectioned_expression( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, + section_result: &sugar::SectionResult, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut current = expected; + let mut parameters = vec![]; + + for §ion_id in section_result.iter() { + let decomposed = + toolkit::decompose_function(state, context, current, toolkit::SynthesiseFunction::Yes)?; + if let Some((argument_type, result_type)) = decomposed { + state.term_scope.bind_section(section_id, argument_type); + parameters.push(argument_type); + current = result_type; } else { + let parameter = state.fresh_unification_type(context); + state.term_scope.bind_section(section_id, parameter); + parameters.push(parameter); + } + } + + let result_type = infer_expression_core(state, context, expression)?; + let result_type = toolkit::instantiate_constrained(state, result_type); + + let _ = unification::subtype(state, context, result_type, current)?; + + let function_type = state.make_function(¶meters, result_type); + Ok(function_type) +} + +fn check_expression_core( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(kind) = context.lowered.info.get_expression_kind(expression) else { + return Ok(context.prim.unknown); + }; + + match kind { + lowering::ExpressionKind::Lambda { binders, expression } => { + check_lambda(state, context, binders, *expression, expected) + } + lowering::ExpressionKind::IfThenElse { if_, then, else_ } => { + check_if_then_else(state, context, *if_, *then, *else_, expected) + } + lowering::ExpressionKind::CaseOf { trunk, branches } => { + check_case_of(state, context, trunk, branches, expected) + } + lowering::ExpressionKind::LetIn { bindings, expression } => { + check_let_in(state, context, bindings, *expression, expected) + } + lowering::ExpressionKind::Parenthesized { parenthesized } => { + let Some(parenthesized) = parenthesized else { return Ok(context.prim.unknown) }; + check_expression(state, context, *parenthesized, expected) + } + lowering::ExpressionKind::Array { array } => check_array(state, context, array, expected), + lowering::ExpressionKind::Record { record } => { + check_record(state, context, record, expected) + } + _ => { + let inferred = infer_expression_quiet(state, context, expression)?; let inferred = toolkit::instantiate_constrained(state, inferred); - unification::subtype_with_mode( - state, - context, - inferred, - expected, - ElaborationMode::No, - )?; + + let _ = unification::subtype(state, context, inferred, expected)?; + crate::trace_fields!(state, context, { inferred = inferred, expected = expected }); Ok(inferred) } - }) + } } /// Infers the type of an expression. @@ -102,13 +137,13 @@ where pub fn infer_expression( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, ) -> QueryResult where Q: ExternalQueries, { - state.with_error_step(ErrorStep::InferringExpression(expr_id), |state| { - let inferred = infer_expression_quiet(state, context, expr_id)?; + state.with_error_step(ErrorStep::InferringExpression(expression), |state| { + let inferred = infer_expression_quiet(state, context, expression)?; crate::trace_fields!(state, context, { inferred = inferred }); Ok(inferred) }) @@ -117,22 +152,22 @@ where fn infer_expression_quiet( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, ) -> QueryResult where Q: ExternalQueries, { - if let Some(section_result) = context.sectioned.expressions.get(&expr_id) { - infer_sectioned_expression(state, context, expr_id, section_result) + if let Some(section_result) = context.sectioned.expressions.get(&expression) { + infer_sectioned_expression(state, context, expression, section_result) } else { - infer_expression_core(state, context, expr_id) + infer_expression_core(state, context, expression) } } fn infer_sectioned_expression( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, section_result: &sugar::SectionResult, ) -> QueryResult where @@ -146,7 +181,7 @@ where let parameter_types = parameter_types.collect_vec(); - let result_type = infer_expression_core(state, context, expr_id)?; + let result_type = infer_expression_core(state, context, expression)?; let result_type = toolkit::instantiate_constrained(state, result_type); Ok(state.make_function(¶meter_types, result_type)) @@ -155,14 +190,14 @@ where fn infer_expression_core( state: &mut CheckState, context: &CheckContext, - expr_id: lowering::ExpressionId, + expression: lowering::ExpressionId, ) -> QueryResult where Q: ExternalQueries, { let unknown = context.prim.unknown; - let Some(kind) = context.lowered.info.get_expression_kind(expr_id) else { + let Some(kind) = context.lowered.info.get_expression_kind(expression) else { return Ok(unknown); }; @@ -178,7 +213,7 @@ where } lowering::ExpressionKind::OperatorChain { .. } => { - let (_, inferred_type) = operator::infer_operator_chain(state, context, expr_id)?; + let (_, inferred_type) = operator::infer_operator_chain(state, context, expression)?; Ok(inferred_type) } @@ -251,7 +286,7 @@ where } lowering::ExpressionKind::Section => { - if let Some(type_id) = state.term_scope.lookup_section(expr_id) { + if let Some(type_id) = state.term_scope.lookup_section(expression) { Ok(type_id) } else { Ok(unknown) @@ -393,6 +428,249 @@ where } } +fn check_lambda( + state: &mut CheckState, + context: &CheckContext, + binders: &[lowering::BinderId], + expression: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + let mut remaining = expected; + + for &binder_id in binders.iter() { + let decomposed = toolkit::decompose_function( + state, + context, + remaining, + toolkit::SynthesiseFunction::Yes, + )?; + if let Some((argument, result)) = decomposed { + let _ = binder::check_binder(state, context, binder_id, argument)?; + arguments.push(argument); + remaining = result; + } else { + let argument_type = state.fresh_unification_type(context); + let _ = binder::check_binder(state, context, binder_id, argument_type)?; + arguments.push(argument_type); + } + } + + let result_type = if let Some(body) = expression { + check_expression(state, context, body, remaining)? + } else { + state.fresh_unification_type(context) + }; + + let function_type = state.make_function(&arguments, result_type); + + let exhaustiveness = + exhaustiveness::check_lambda_patterns(state, context, &arguments, binders)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } + + Ok(function_type) +} + +fn check_if_then_else( + state: &mut CheckState, + context: &CheckContext, + if_: Option, + then: Option, + else_: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(if_) = if_ { + check_expression(state, context, if_, context.prim.boolean)?; + } + + if let Some(then) = then { + check_expression(state, context, then, expected)?; + } + + if let Some(else_) = else_ { + check_expression(state, context, else_, expected)?; + } + + Ok(expected) +} + +fn check_case_of( + state: &mut CheckState, + context: &CheckContext, + trunk: &[lowering::ExpressionId], + branches: &[lowering::CaseBranch], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut trunk_types = vec![]; + for trunk in trunk.iter() { + let trunk_type = infer_expression(state, context, *trunk)?; + trunk_types.push(trunk_type); + } + + for branch in branches.iter() { + for (binder, trunk) in branch.binders.iter().zip(&trunk_types) { + let _ = binder::check_binder(state, context, *binder, *trunk)?; + } + if let Some(guarded) = &branch.guarded_expression { + check_guarded_expression(state, context, guarded, expected)?; + } + } + + let exhaustiveness = + exhaustiveness::check_case_patterns(state, context, &trunk_types, branches)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } + + Ok(expected) +} + +fn check_let_in( + state: &mut CheckState, + context: &CheckContext, + bindings: &[lowering::LetBindingChunk], + expression: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + check_let_chunks(state, context, bindings)?; + + let Some(expression) = expression else { + return Ok(context.prim.unknown); + }; + + check_expression(state, context, expression, expected) +} + +fn check_array( + state: &mut CheckState, + context: &CheckContext, + array: &[lowering::ExpressionId], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let normalised = state.normalize_type(expected); + if let Type::Application(constructor, element_type) = state.storage[normalised] { + let constructor = state.normalize_type(constructor); + if constructor == context.prim.array { + for expression in array.iter() { + check_expression(state, context, *expression, element_type)?; + } + return Ok(expected); + } + } + + // Fallback: infer then subtype. + let inferred = infer_array(state, context, array)?; + let _ = unification::subtype(state, context, inferred, expected)?; + Ok(inferred) +} + +fn check_record( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::ExpressionRecordItem], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let normalised = state.normalize_type(expected); + if let Type::Application(constructor, row_type) = state.storage[normalised] { + let constructor = state.normalize_type(constructor); + if constructor == context.prim.record { + let row_type = state.normalize_type(row_type); + if let Type::Row(ref row) = state.storage[row_type] { + let expected_fields = row.fields.clone(); + + let mut fields = vec![]; + + for field in record.iter() { + match field { + lowering::ExpressionRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + + let label = SmolStr::clone(name); + + // Look up whether this field has an expected type. + let expected_field_type = + expected_fields.iter().find(|f| f.label == label).map(|f| f.id); + + let id = if let Some(expected_type) = expected_field_type { + check_expression(state, context, *value, expected_type)? + } else { + let id = infer_expression(state, context, *value)?; + let id = toolkit::instantiate_forall(state, id); + toolkit::collect_constraints(state, id) + }; + + fields.push(RowField { label, id }); + } + lowering::ExpressionRecordItem::RecordPun { name, resolution } => { + let Some(name) = name else { continue }; + let Some(resolution) = resolution else { continue }; + + let label = SmolStr::clone(name); + + let expected_field_type = + expected_fields.iter().find(|f| f.label == label).map(|f| f.id); + + let id = lookup_term_variable(state, context, *resolution)?; + + let id = if let Some(expected_type) = expected_field_type { + let _ = unification::subtype(state, context, id, expected_type)?; + id + } else { + let id = toolkit::instantiate_forall(state, id); + toolkit::collect_constraints(state, id) + }; + + fields.push(RowField { label, id }); + } + } + } + + let row_type = RowType::from_unsorted(fields, None); + let row_type = state.storage.intern(Type::Row(row_type)); + let record_type = + state.storage.intern(Type::Application(context.prim.record, row_type)); + + let _ = unification::subtype(state, context, record_type, expected)?; + return Ok(record_type); + } + } + } + + // Fallback: infer then subtype. + let inferred = infer_record(state, context, record)?; + let _ = unification::subtype(state, context, inferred, expected)?; + Ok(inferred) +} + fn infer_case_of( state: &mut CheckState, context: &CheckContext, @@ -1647,13 +1925,7 @@ pub fn check_function_term_application( where Q: ExternalQueries, { - check_function_application_core( - state, - context, - function_t, - expression_id, - check_expression_argument, - ) + check_function_application_core(state, context, function_t, expression_id, check_expression) } pub(crate) fn check_let_chunks( diff --git a/tests-integration/fixtures/checking/043_binder_named/Main.snap b/tests-integration/fixtures/checking/043_binder_named/Main.snap index f8f87d19..309efad3 100644 --- a/tests-integration/fixtures/checking/043_binder_named/Main.snap +++ b/tests-integration/fixtures/checking/043_binder_named/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -7,9 +8,9 @@ foo :: Int -> Int bar :: String -> String baz :: Int -> Int qux :: Int -foo' :: forall (t3 :: Type). (t3 :: Type) -> (t3 :: Type) -bar' :: forall (t6 :: Type). (t6 :: Type) -> (t6 :: Type) -baz' :: forall (t9 :: Type). (t9 :: Type) -> (t9 :: Type) +foo' :: forall (t2 :: Type). (t2 :: Type) -> (t2 :: Type) +bar' :: forall (t5 :: Type). (t5 :: Type) -> (t5 :: Type) +baz' :: forall (t8 :: Type). (t8 :: Type) -> (t8 :: Type) qux' :: Int Types diff --git a/tests-integration/fixtures/checking/060_array_binder/Main.snap b/tests-integration/fixtures/checking/060_array_binder/Main.snap index 62e3b29b..a2f3d171 100644 --- a/tests-integration/fixtures/checking/060_array_binder/Main.snap +++ b/tests-integration/fixtures/checking/060_array_binder/Main.snap @@ -7,11 +7,11 @@ Terms test1 :: Array Int -> { x :: Int, y :: Int } test1' :: forall (t6 :: Type). Array (t6 :: Type) -> { x :: (t6 :: Type), y :: (t6 :: Type) } test2 :: forall (a :: Type). Array (a :: Type) -> Array (a :: Type) -test2' :: forall (t21 :: Type). Array (t21 :: Type) -> Array (t21 :: Type) +test2' :: forall (t20 :: Type). Array (t20 :: Type) -> Array (t20 :: Type) test3 :: Array Int -> Int test3' :: Array Int -> Int nested :: Array (Array Int) -> Int -nested' :: forall (t53 :: Type). Array (Array (t53 :: Type)) -> (t53 :: Type) +nested' :: forall (t52 :: Type). Array (Array (t52 :: Type)) -> (t52 :: Type) Types diff --git a/tests-integration/fixtures/checking/062_case_of/Main.snap b/tests-integration/fixtures/checking/062_case_of/Main.snap index 248a51db..99df9e30 100644 --- a/tests-integration/fixtures/checking/062_case_of/Main.snap +++ b/tests-integration/fixtures/checking/062_case_of/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -13,7 +14,7 @@ test1' :: Maybe Int -> Int test2 :: Maybe Int -> Maybe Int -> Int test2' :: Maybe Int -> Maybe Int -> Int test3 :: Either Int String -> Int -test3' :: forall (t46 :: Type). Either Int (t46 :: Type) -> Int +test3' :: forall (t43 :: Type). Either Int (t43 :: Type) -> Int test4 :: Tuple (Maybe Int) (Maybe Int) -> Int test4' :: Tuple (Maybe Int) (Maybe Int) -> Int diff --git a/tests-integration/fixtures/checking/068_expression_sections/Main.snap b/tests-integration/fixtures/checking/068_expression_sections/Main.snap index 5d3feda1..361326ff 100644 --- a/tests-integration/fixtures/checking/068_expression_sections/Main.snap +++ b/tests-integration/fixtures/checking/068_expression_sections/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,14 +22,14 @@ test7 :: Int -> Int test8 :: Int -> Int test9 :: Int -> Int -> Int test10 :: forall (t41 :: Type). ((Int -> Int) -> (t41 :: Type)) -> (t41 :: Type) -test11 :: forall (t48 :: Type). (t48 :: Type) -> Array (t48 :: Type) -test12 :: forall (t51 :: Type). (t51 :: Type) -> { foo :: (t51 :: Type) } -test13 :: forall (t54 :: Type). (t54 :: Type) -> (t54 :: Type) -test14 :: forall (t59 :: Type). (t59 :: Type) -> (t59 :: Type) -> Array (t59 :: Type) +test11 :: forall (t49 :: Type). (t49 :: Type) -> Array (t49 :: Type) +test12 :: forall (t52 :: Type). (t52 :: Type) -> { foo :: (t52 :: Type) } +test13 :: forall (t55 :: Type). (t55 :: Type) -> (t55 :: Type) +test14 :: forall (t60 :: Type). (t60 :: Type) -> (t60 :: Type) -> Array (t60 :: Type) test15 :: - forall (t62 :: Type) (t63 :: Type). - (t62 :: Type) -> (t63 :: Type) -> { a :: (t62 :: Type), b :: (t63 :: Type) } -test16 :: forall (t67 :: Type). (t67 :: Type) -> Tuple (t67 :: Type) Int + forall (t63 :: Type) (t64 :: Type). + (t63 :: Type) -> (t64 :: Type) -> { a :: (t63 :: Type), b :: (t64 :: Type) } +test16 :: forall (t68 :: Type). (t68 :: Type) -> Tuple (t68 :: Type) Int test17 :: Tuple String Int test18 :: Int -> Int diff --git a/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap b/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap index 8504e122..d4bf5280 100644 --- a/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap +++ b/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -10,8 +11,8 @@ sub :: Int -> Int -> Int identity :: forall (a :: Type). (a :: Type) -> (a :: Type) test1 :: Int -> String -> { a :: Int, b :: String } test2 :: - forall (t13 :: Type) (t16 :: Type) (t17 :: Type). - ((((t16 :: Type) -> (t17 :: Type)) -> (t16 :: Type) -> (t17 :: Type)) -> (t13 :: Type)) -> + forall (t13 :: Type) (t17 :: Type) (t18 :: Type). + ((((t18 :: Type) -> (t17 :: Type)) -> (t18 :: Type) -> (t17 :: Type)) -> (t13 :: Type)) -> (t13 :: Type) test3 :: Boolean -> Int -> Int test4 :: Int diff --git a/tests-integration/fixtures/checking/070_record_access_sections/Main.snap b/tests-integration/fixtures/checking/070_record_access_sections/Main.snap index e1dbcd67..a1b02114 100644 --- a/tests-integration/fixtures/checking/070_record_access_sections/Main.snap +++ b/tests-integration/fixtures/checking/070_record_access_sections/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,55 +10,54 @@ map :: apply :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) f :: ({ foo :: Int } -> Int) -> Int test1 :: - forall (t9 :: Type) (t10 :: Row Type). - { prop :: (t9 :: Type) | (t10 :: Row Type) } -> (t9 :: Type) + forall (t8 :: Type) (t9 :: Row Type). { prop :: (t8 :: Type) | (t9 :: Row Type) } -> (t8 :: Type) test2 :: - forall (t15 :: Row Type) (t17 :: Row Type) (t18 :: Type) (t19 :: Row Type). - { a :: { b :: { c :: (t18 :: Type) | (t19 :: Row Type) } | (t17 :: Row Type) } - | (t15 :: Row Type) + forall (t14 :: Row Type) (t16 :: Row Type) (t17 :: Type) (t18 :: Row Type). + { a :: { b :: { c :: (t17 :: Type) | (t18 :: Row Type) } | (t16 :: Row Type) } + | (t14 :: Row Type) } -> - (t18 :: Type) + (t17 :: Type) test3 :: - forall (t23 :: Type) (t24 :: Row Type). - { poly :: (t23 :: Type) | (t24 :: Row Type) } -> (t23 :: Type) + forall (t22 :: Type) (t23 :: Row Type). + { poly :: (t22 :: Type) | (t23 :: Row Type) } -> (t22 :: Type) test4 :: - forall (t28 :: Type) (t31 :: Row Type). - Array { prop :: (t28 :: Type) | (t31 :: Row Type) } -> Array (t28 :: Type) + forall (t27 :: Type) (t29 :: Row Type). + Array { prop :: (t27 :: Type) | (t29 :: Row Type) } -> Array (t27 :: Type) test5 :: Int test6 :: - forall (t39 :: Type) (t41 :: Type) (t45 :: Type) (t46 :: Row Type). - (t39 :: Type) -> - (({ bar :: (t45 :: Type) | (t46 :: Row Type) } -> (t45 :: Type)) -> (t41 :: Type)) -> - (t41 :: Type) + forall (t36 :: Type) (t38 :: Type) (t42 :: Type) (t44 :: Row Type). + (t36 :: Type) -> + (({ bar :: (t42 :: Type) | (t44 :: Row Type) } -> (t42 :: Type)) -> (t38 :: Type)) -> + (t38 :: Type) test7 :: - forall (t51 :: Type) (t52 :: Row Type). - { nonexistent :: (t51 :: Type) | (t52 :: Row Type) } -> (t51 :: Type) + forall (t49 :: Type) (t50 :: Row Type). + { nonexistent :: (t49 :: Type) | (t50 :: Row Type) } -> (t49 :: Type) test8 :: - forall (t57 :: Type) (t59 :: Type) (t60 :: Row Type). - (({ foo :: (t59 :: Type) | (t60 :: Row Type) } -> (t59 :: Type)) -> (t57 :: Type)) -> - (t57 :: Type) + forall (t55 :: Type) (t57 :: Type) (t59 :: Row Type). + (({ foo :: (t57 :: Type) | (t59 :: Row Type) } -> (t57 :: Type)) -> (t55 :: Type)) -> + (t55 :: Type) test9 :: - forall (t64 :: Type) (t69 :: Type) (t70 :: Row Type). - Array (({ prop :: (t69 :: Type) | (t70 :: Row Type) } -> (t69 :: Type)) -> (t64 :: Type)) -> - Array (t64 :: Type) + forall (t63 :: Type) (t67 :: Type) (t69 :: Row Type). + Array (({ prop :: (t67 :: Type) | (t69 :: Row Type) } -> (t67 :: Type)) -> (t63 :: Type)) -> + Array (t63 :: Type) test10 :: - forall (t74 :: Type) (t77 :: Row Type) (t79 :: Row Type). - { a :: { b :: (t74 :: Type) | (t79 :: Row Type) } | (t77 :: Row Type) } -> (t74 :: Type) + forall (t73 :: Type) (t75 :: Row Type) (t77 :: Row Type). + { a :: { b :: (t73 :: Type) | (t77 :: Row Type) } | (t75 :: Row Type) } -> (t73 :: Type) test11 :: - forall (t82 :: Type) (t84 :: Type) (t85 :: Row Type). - (t82 :: Type) -> - { a :: (t82 :: Type), b :: { foo :: (t84 :: Type) | (t85 :: Row Type) } -> (t84 :: Type) } + forall (t80 :: Type) (t82 :: Type) (t83 :: Row Type). + (t80 :: Type) -> + { a :: (t80 :: Type), b :: { foo :: (t82 :: Type) | (t83 :: Row Type) } -> (t82 :: Type) } test12 :: - forall (t91 :: Type) (t92 :: Row Type). - ({ bar :: (t91 :: Type) | (t92 :: Row Type) } -> (t91 :: Type)) -> - Array ({ bar :: (t91 :: Type) | (t92 :: Row Type) } -> (t91 :: Type)) + forall (t89 :: Type) (t90 :: Row Type). + ({ bar :: (t89 :: Type) | (t90 :: Row Type) } -> (t89 :: Type)) -> + Array ({ bar :: (t89 :: Type) | (t90 :: Row Type) } -> (t89 :: Type)) test13 :: - forall (t95 :: Type) (t98 :: Type) (t99 :: Row Type). - (t95 :: Type) -> { prop :: (t98 :: Type) | (t99 :: Row Type) } -> (t98 :: Type) + forall (t93 :: Type) (t96 :: Type) (t97 :: Row Type). + (t93 :: Type) -> { prop :: (t96 :: Type) | (t97 :: Row Type) } -> (t96 :: Type) test14 :: - forall (t105 :: Type) (t110 :: Row Type). + forall (t103 :: Type) (t108 :: Row Type). Boolean -> - { a :: (t105 :: Type) | ( b :: (t105 :: Type) | (t110 :: Row Type) ) } -> - (t105 :: Type) + { a :: (t103 :: Type) | ( b :: (t103 :: Type) | (t108 :: Row Type) ) } -> + (t103 :: Type) Types diff --git a/tests-integration/fixtures/checking/071_record_update_sections/Main.snap b/tests-integration/fixtures/checking/071_record_update_sections/Main.snap index 17a42b0e..285e6adc 100644 --- a/tests-integration/fixtures/checking/071_record_update_sections/Main.snap +++ b/tests-integration/fixtures/checking/071_record_update_sections/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -28,28 +29,28 @@ polymorphicRecordUpdate :: forall (t36 :: Type) (t37 :: Row Type). { foo :: (t36 :: Type) | (t37 :: Row Type) } -> { foo :: Int | (t37 :: Row Type) } higherOrderContext :: - forall (t44 :: Type) (t45 :: Row Type). - Array { x :: (t44 :: Type) | (t45 :: Row Type) } -> Array { x :: Int | (t45 :: Row Type) } + forall (t42 :: Type) (t43 :: Row Type). + Array { x :: (t42 :: Type) | (t43 :: Row Type) } -> Array { x :: Int | (t43 :: Row Type) } multipleSectionsInteraction :: - forall (t49 :: Type) (t50 :: Type) (t51 :: Type) (t52 :: Type) (t53 :: Row Type). - { x :: (t51 :: Type), y :: (t52 :: Type) | (t53 :: Row Type) } -> - (t49 :: Type) -> - (t50 :: Type) -> - { x :: (t49 :: Type), y :: (t50 :: Type) | (t53 :: Row Type) } + forall (t47 :: Type) (t48 :: Type) (t49 :: Type) (t50 :: Type) (t51 :: Row Type). + { x :: (t49 :: Type), y :: (t50 :: Type) | (t51 :: Row Type) } -> + (t47 :: Type) -> + (t48 :: Type) -> + { x :: (t47 :: Type), y :: (t48 :: Type) | (t51 :: Row Type) } nestedSectionInteraction :: - forall (t57 :: Type) (t58 :: Type) (t59 :: Row Type) (t60 :: Row Type). - { a :: { b :: (t58 :: Type) | (t59 :: Row Type) } | (t60 :: Row Type) } -> - (t57 :: Type) -> - { a :: { b :: (t57 :: Type) | (t59 :: Row Type) } | (t60 :: Row Type) } + forall (t55 :: Type) (t56 :: Type) (t57 :: Row Type) (t58 :: Row Type). + { a :: { b :: (t56 :: Type) | (t57 :: Row Type) } | (t58 :: Row Type) } -> + (t55 :: Type) -> + { a :: { b :: (t55 :: Type) | (t57 :: Row Type) } | (t58 :: Row Type) } mixedSections :: - forall (t64 :: Type) (t66 :: Type) (t67 :: Row Type). - { a :: (t64 :: Type), b :: (t66 :: Type) | (t67 :: Row Type) } -> - { a :: Int -> Int, b :: Int | (t67 :: Row Type) } + forall (t62 :: Type) (t64 :: Type) (t65 :: Row Type). + { a :: (t62 :: Type), b :: (t64 :: Type) | (t65 :: Row Type) } -> + { a :: Int -> Int, b :: Int | (t65 :: Row Type) } recordAccessSectionUpdate :: - forall (t71 :: Type) (t73 :: Type) (t74 :: Row Type) (t75 :: Row Type). - { a :: (t71 :: Type) | (t75 :: Row Type) } -> - { a :: { b :: (t73 :: Type) | (t74 :: Row Type) } -> (t73 :: Type) | (t75 :: Row Type) } -concreteRecordUpdateSection :: forall (t78 :: Type). (t78 :: Type) -> { a :: (t78 :: Type) | () } + forall (t69 :: Type) (t71 :: Type) (t72 :: Row Type) (t73 :: Row Type). + { a :: (t69 :: Type) | (t73 :: Row Type) } -> + { a :: { b :: (t71 :: Type) | (t72 :: Row Type) } -> (t71 :: Type) | (t73 :: Row Type) } +concreteRecordUpdateSection :: forall (t76 :: Type). (t76 :: Type) -> { a :: (t76 :: Type) | () } map :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> Array (a :: Type) -> Array (b :: Type) diff --git a/tests-integration/fixtures/checking/102_builtin_row/Main.snap b/tests-integration/fixtures/checking/102_builtin_row/Main.snap index 7725ef7b..bf9a2e59 100644 --- a/tests-integration/fixtures/checking/102_builtin_row/Main.snap +++ b/tests-integration/fixtures/checking/102_builtin_row/Main.snap @@ -75,11 +75,11 @@ rowToListThree :: RowToList @Type ( a :: Int, b :: String, c :: Boolean ) (list :: RowList Type) => Proxy @(RowList Type) (list :: RowList Type) solveUnion :: - forall (t147 :: Type). + forall (t145 :: Type). { deriveUnion :: Proxy @(Row Type) ( a :: Int, b :: String ) , deriveUnionLeft :: Proxy @(Row Type) ( a :: Int ) , deriveUnionRight :: Proxy @(Row Type) ( b :: String ) - , unionBothEmpty :: Proxy @(Row (t147 :: Type)) () + , unionBothEmpty :: Proxy @(Row (t145 :: Type)) () , unionEmptyLeft :: Proxy @(Row Type) ( a :: Int ) , unionEmptyRight :: Proxy @(Row Type) ( a :: Int ) , unionMultiple :: Proxy @(Row Type) ( a :: Int, b :: String, c :: Boolean ) @@ -91,18 +91,18 @@ solveCons :: , nestedCons :: Proxy @(Row Type) ( a :: Int, b :: String ) } solveLacks :: - forall (t160 :: Type) (t161 :: (t160 :: Type)) (t164 :: Type) (t166 :: (t164 :: Type)). - { lacksEmpty :: Proxy @(t164 :: Type) (t166 :: (t164 :: Type)) - , lacksSimple :: Proxy @(t160 :: Type) (t161 :: (t160 :: Type)) + forall (t158 :: Type) (t159 :: (t158 :: Type)) (t162 :: Type) (t164 :: (t162 :: Type)). + { lacksEmpty :: Proxy @(t162 :: Type) (t164 :: (t162 :: Type)) + , lacksSimple :: Proxy @(t158 :: Type) (t159 :: (t158 :: Type)) } solveNub :: - forall (t175 :: Type). - { nubEmpty :: Proxy @(Row (t175 :: Type)) () + forall (t173 :: Type). + { nubEmpty :: Proxy @(Row (t173 :: Type)) () , nubNoDuplicates :: Proxy @(Row Type) ( a :: Int, b :: String ) } solveRowToList :: - forall (t183 :: Type). - { rowToListEmpty :: Proxy @(RowList (t183 :: Type)) (Nil @(t183 :: Type)) + forall (t181 :: Type). + { rowToListEmpty :: Proxy @(RowList (t181 :: Type)) (Nil @(t181 :: Type)) , rowToListMultiple :: Proxy @(RowList Type) (Cons @Type "a" Int (Cons @Type "b" String (Nil @Type))) , rowToListSimple :: Proxy @(RowList Type) (Cons @Type "a" Int (Nil @Type)) diff --git a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap index d269318c..b8eaddc3 100644 --- a/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap +++ b/tests-integration/fixtures/checking/125_instance_member_overly_general/Main.snap @@ -17,6 +17,16 @@ instance Show (Boolean :: Type) chain: 0 Diagnostics +error[CannotUnify]: Cannot unify '(~a :: Type)' with 'Boolean' + --> 6:1..8:18 + | +6 | instance Show Boolean where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify '(~a :: Type) -> String' with 'Boolean -> String' + --> 6:1..8:18 + | +6 | instance Show Boolean where + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ error[CannotUnify]: Cannot unify 'forall (a :: Type). (a :: Type) -> String' with 'Boolean -> String' --> 6:1..8:18 | diff --git a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap index d214fb2b..224f707c 100644 --- a/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap +++ b/tests-integration/fixtures/checking/231_record_expression_nested_additional/Main.snap @@ -9,6 +9,11 @@ test :: { outer :: { x :: Int } } Types Diagnostics +error[AdditionalProperty]: Additional properties not allowed: y + --> 4:17..4:31 + | +4 | test = { outer: { x: 1, y: 2 } } + | ^~~~~~~~~~~~~~ error[AdditionalProperty]: Additional properties not allowed: y --> 4:8..4:33 | diff --git a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap index 5391a7ce..69264ec1 100644 --- a/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap +++ b/tests-integration/fixtures/checking/273_class_member_instantiation/Main.snap @@ -19,14 +19,14 @@ test1 :: forall (a :: Type). Monoid (a :: Type) => Unit -> (a :: Type) test2 :: forall (t22 :: Type) (t23 :: Type). Monoid (t23 :: Type) => (t22 :: Type) -> (t23 :: Type) test3 :: forall (t24 :: Type). Monoid (t24 :: Type) => (t24 :: Type) test4 :: - forall (t32 :: Type -> Type) (t33 :: Type). - Monoid (t33 :: Type) => (t32 :: Type -> Type) (t33 :: Type) + forall (t31 :: Type -> Type) (t32 :: Type). + Monoid (t32 :: Type) => (t31 :: Type -> Type) (t32 :: Type) test5 :: - forall (t37 :: Type -> Type) (t40 :: Type). - Semigroup (t40 :: Type) => - (t37 :: Type -> Type) (t40 :: Type) -> - (t37 :: Type -> Type) (t40 :: Type) -> - (t37 :: Type -> Type) (t40 :: Type) + forall (t36 :: Type -> Type) (t39 :: Type). + Semigroup (t39 :: Type) => + (t36 :: Type -> Type) (t39 :: Type) -> + (t36 :: Type -> Type) (t39 :: Type) -> + (t36 :: Type -> Type) (t39 :: Type) Types Unit :: Type diff --git a/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap index de58793b..80094374 100644 --- a/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap +++ b/tests-integration/fixtures/checking/291_compose_constraint_discharge/Main.snap @@ -18,8 +18,8 @@ test :: forall (a :: Type) (b :: Type). (Array (a :: Type) -> Maybe (b :: Type)) -> Maybe (a :: Type) -> (b :: Type) test' :: - forall (t25 :: Type) (t30 :: Type). - (Array (t30 :: Type) -> Maybe (t25 :: Type)) -> Maybe (t30 :: Type) -> (t25 :: Type) + forall (t24 :: Type) (t29 :: Type). + (Array (t29 :: Type) -> Maybe (t24 :: Type)) -> Maybe (t29 :: Type) -> (t24 :: Type) Types Maybe :: Type -> Type From b208f21a160d6ce85ee09c29b9a7f69079d357f4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 22:36:18 +0800 Subject: [PATCH 152/386] Add missing unification rules for Forall --- .../checking/src/algorithm/unification.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index e77f6ace..58e69835 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -231,6 +231,22 @@ where unify(state, context, t1, t2)? } + (Type::Forall(binder, inner), _) => { + let name = state.fresh_name(&binder.text); + let skolem = Variable::Skolem(name, binder.kind); + let skolem = state.storage.intern(Type::Variable(skolem)); + let inner = substitute::SubstituteBound::on(state, binder.variable, skolem, inner); + unify(state, context, inner, t2)? + } + + (_, Type::Forall(binder, inner)) => { + let name = state.fresh_name(&binder.text); + let skolem = Variable::Skolem(name, binder.kind); + let skolem = state.storage.intern(Type::Variable(skolem)); + let inner = substitute::SubstituteBound::on(state, binder.variable, skolem, inner); + unify(state, context, t1, inner)? + } + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { unify(state, context, t1_argument, t2_argument)? && unify(state, context, t1_result, t2_result)? From c24b486f97f74f004ae1aae098d74a4d95cdbdac Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 22:38:53 +0800 Subject: [PATCH 153/386] Move normalise_expand_type to toolkit --- compiler-core/checking/src/algorithm.rs | 3 -- .../checking/src/algorithm/binder.rs | 8 +++--- .../checking/src/algorithm/derive.rs | 4 +-- .../src/algorithm/exhaustiveness/convert.rs | 4 +-- .../checking/src/algorithm/inspect.rs | 6 ++-- .../checking/src/algorithm/normalise.rs | 28 ------------------- compiler-core/checking/src/algorithm/term.rs | 8 +++--- .../checking/src/algorithm/term_item.rs | 5 ++-- .../checking/src/algorithm/toolkit.rs | 22 +++++++++++++++ .../checking/src/algorithm/unification.rs | 14 +++++----- 10 files changed, 46 insertions(+), 56 deletions(-) delete mode 100644 compiler-core/checking/src/algorithm/normalise.rs diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 5a646f12..82b674c9 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -29,9 +29,6 @@ pub mod inspect; /// Implements kind inference and checking for [`lowering::TypeKind`]. pub mod kind; -/// Implements type normalisation for unification variables, operators, synonyms. -pub mod normalise; - /// Implements surface-generic operator chain inference. pub mod operator; diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index 92625b3a..dad5438a 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -7,7 +7,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::unification::ElaborationMode; -use crate::algorithm::{binder, kind, normalise, operator, term, toolkit, unification}; +use crate::algorithm::{binder, kind, operator, term, toolkit, unification}; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::{ErrorKind, ErrorStep}; @@ -389,14 +389,14 @@ fn extract_expected_row( where Q: ExternalQueries, { - let expected_type = normalise::normalise_expand_type(state, context, expected_type)?; + let expected_type = toolkit::normalise_expand_type(state, context, expected_type)?; let &Type::Application(function, argument) = &state.storage[expected_type] else { return Ok(None); }; if function != context.prim.record { return Ok(None); } - let row = normalise::normalise_expand_type(state, context, argument)?; + let row = toolkit::normalise_expand_type(state, context, argument)?; let Type::Row(row) = &state.storage[row] else { return Ok(None); }; @@ -416,7 +416,7 @@ where { let pattern_items = collect_pattern_items(record); - let expected_type = normalise::normalise_expand_type(state, context, expected_type)?; + let expected_type = toolkit::normalise_expand_type(state, context, expected_type)?; let expected_row = if let &Type::Application(function, _) = &state.storage[expected_type] && function == context.prim.record diff --git a/compiler-core/checking/src/algorithm/derive.rs b/compiler-core/checking/src/algorithm/derive.rs index be111ee0..9ba608d1 100644 --- a/compiler-core/checking/src/algorithm/derive.rs +++ b/compiler-core/checking/src/algorithm/derive.rs @@ -20,7 +20,7 @@ use crate::ExternalQueries; use crate::algorithm::derive::variance::VarianceConfig; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{kind, normalise, term_item, toolkit, transfer}; +use crate::algorithm::{kind, term_item, toolkit, transfer}; use crate::core::{Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -275,7 +275,7 @@ where }; // Make sure that the constraint solver sees the synonym expansion. - let inner_type = normalise::normalise_expand_type(state, context, inner_type)?; + let inner_type = toolkit::normalise_expand_type(state, context, inner_type)?; let delegate_constraint = { let class_type = state.storage.intern(Type::Constructor(input.class_file, input.class_id)); diff --git a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs index 3b77ca18..4c22fbe7 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness/convert.rs @@ -12,7 +12,7 @@ use sugar::OperatorTree; use crate::ExternalQueries; use crate::algorithm::exhaustiveness::{PatternConstructor, PatternId}; use crate::algorithm::state::{CheckContext, CheckState, OperatorBranchTypes}; -use crate::algorithm::{normalise, toolkit}; +use crate::algorithm::toolkit; use crate::core::{Type, TypeId}; pub fn convert_binder( @@ -136,7 +136,7 @@ fn try_build_record_constructor( where Q: ExternalQueries, { - let expanded_t = normalise::normalise_expand_type(state, context, t)?; + let expanded_t = toolkit::normalise_expand_type(state, context, t)?; let (constructor, arguments) = toolkit::extract_type_application(state, expanded_t); diff --git a/compiler-core/checking/src/algorithm/inspect.rs b/compiler-core/checking/src/algorithm/inspect.rs index 230dd5ee..c49fbac2 100644 --- a/compiler-core/checking/src/algorithm/inspect.rs +++ b/compiler-core/checking/src/algorithm/inspect.rs @@ -6,7 +6,7 @@ use lowering::TypeVariableBindingId; use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{normalise, substitute}; +use crate::algorithm::{substitute, toolkit}; use crate::core::{ForallBinder, Type, TypeId, Variable}; pub struct InspectSignature { @@ -81,7 +81,7 @@ where let mut bindings = substitute::NameToType::default(); safe_loop! { - current_id = normalise::normalise_expand_type(state, context, current_id)?; + current_id = toolkit::normalise_expand_type(state, context, current_id)?; // In the example, after the Forall case has peeled f, g, and the // synonym-expanded a, the accumulated bindings are the following: @@ -153,7 +153,7 @@ where let mut bindings = substitute::NameToType::default(); safe_loop! { - current_id = normalise::normalise_expand_type(state, context, current_id)?; + current_id = toolkit::normalise_expand_type(state, context, current_id)?; if !matches!(state.storage[current_id], Type::Forall(..)) && !bindings.is_empty() { current_id = substitute::SubstituteBindings::on(state, &bindings, current_id); diff --git a/compiler-core/checking/src/algorithm/normalise.rs b/compiler-core/checking/src/algorithm/normalise.rs deleted file mode 100644 index 175a2c2c..00000000 --- a/compiler-core/checking/src/algorithm/normalise.rs +++ /dev/null @@ -1,28 +0,0 @@ -use building_types::QueryResult; - -use crate::ExternalQueries; -use crate::algorithm::kind::{operator, synonym}; -use crate::algorithm::safety::safe_loop; -use crate::algorithm::state::{CheckContext, CheckState}; -use crate::core::TypeId; - -pub fn normalise_expand_type( - state: &mut CheckState, - context: &CheckContext, - mut type_id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - safe_loop! { - let expanded_id = state.normalize_type(type_id); - let expanded_id = operator::expand_type_operator(state, context, expanded_id)?; - let expanded_id = synonym::expand_type_synonym(state, context, expanded_id)?; - - if expanded_id == type_id { - return Ok(type_id); - } - - type_id = expanded_id; - } -} diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 89881573..deb62f38 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -7,8 +7,8 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{ - binder, equation, exhaustiveness, inspect, kind, normalise, operator, substitute, toolkit, - transfer, unification, + binder, equation, exhaustiveness, inspect, kind, operator, substitute, toolkit, transfer, + unification, }; use crate::core::{RowField, RowType, Type, TypeId}; use crate::error::{ErrorKind, ErrorStep}; @@ -40,7 +40,7 @@ where Q: ExternalQueries, { // TODO: move normalisation to toolkit - let expected = normalise::normalise_expand_type(state, context, expected)?; + let expected = toolkit::normalise_expand_type(state, context, expected)?; let expected = toolkit::skolemise_forall(state, expected); let expected = toolkit::collect_given_constraints(state, expected); if let Some(section_result) = context.sectioned.expressions.get(&expression) { @@ -1837,7 +1837,7 @@ where F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { crate::trace_fields!(state, context, { function = function_t }); - let function_t = normalise::normalise_expand_type(state, context, function_t)?; + let function_t = toolkit::normalise_expand_type(state, context, function_t)?; match state.storage[function_t] { // Check that `argument_id :: argument_type` Type::Function(argument_type, result_type) => { diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index c6e62e1f..149fefbd 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -10,8 +10,7 @@ use crate::ExternalQueries; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState, InstanceHeadBinding, PendingType}; use crate::algorithm::{ - constraint, equation, inspect, kind, normalise, quantify, substitute, term, transfer, - unification, + constraint, equation, inspect, kind, quantify, substitute, term, toolkit, transfer, unification, }; use crate::core::{Instance, InstanceKind, Name, Type, TypeId, Variable, debruijn}; use crate::error::{ErrorKind, ErrorStep}; @@ -241,7 +240,7 @@ where let mut current = class_kind; for _ in 0..FUEL { - current = normalise::normalise_expand_type(state, context, current)?; + current = toolkit::normalise_expand_type(state, context, current)?; match state.storage[current] { Type::Forall(ref binder, inner) => { let binder_variable = binder.variable.clone(); diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 9944f63e..329cc798 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -1,6 +1,7 @@ use building_types::QueryResult; use crate::ExternalQueries; +use crate::algorithm::kind::{operator, synonym}; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::substitute; @@ -271,3 +272,24 @@ where _ => Ok(None), } } + +pub fn normalise_expand_type( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + let expanded_id = state.normalize_type(type_id); + let expanded_id = operator::expand_type_operator(state, context, expanded_id)?; + let expanded_id = synonym::expand_type_synonym(state, context, expanded_id)?; + + if expanded_id == type_id { + return Ok(type_id); + } + + type_id = expanded_id; + } +} diff --git a/compiler-core/checking/src/algorithm/unification.rs b/compiler-core/checking/src/algorithm/unification.rs index 58e69835..f8fa9dcb 100644 --- a/compiler-core/checking/src/algorithm/unification.rs +++ b/compiler-core/checking/src/algorithm/unification.rs @@ -5,7 +5,7 @@ use itertools::{EitherOrBoth, Itertools}; use crate::ExternalQueries; use crate::algorithm::state::{CheckContext, CheckState}; -use crate::algorithm::{kind, normalise, substitute}; +use crate::algorithm::{kind, substitute, toolkit}; use crate::core::{RowField, RowType, Type, TypeId, Variable, debruijn}; use crate::error::ErrorKind; @@ -88,8 +88,8 @@ pub fn subtype_with_mode( where Q: ExternalQueries, { - let t1 = normalise::normalise_expand_type(state, context, t1)?; - let t2 = normalise::normalise_expand_type(state, context, t2)?; + let t1 = toolkit::normalise_expand_type(state, context, t1)?; + let t2 = toolkit::normalise_expand_type(state, context, t2)?; crate::debug_fields!(state, context, { t1 = t1, t2 = t2, ?mode = mode }); @@ -144,8 +144,8 @@ where Type::Application(t1_function, t1_argument), Type::Application(t2_function, t2_argument), ) if t1_function == context.prim.record && t2_function == context.prim.record => { - let t1_argument = normalise::normalise_expand_type(state, context, t1_argument)?; - let t2_argument = normalise::normalise_expand_type(state, context, t2_argument)?; + let t1_argument = toolkit::normalise_expand_type(state, context, t1_argument)?; + let t2_argument = toolkit::normalise_expand_type(state, context, t2_argument)?; let t1_core = state.storage[t1_argument].clone(); let t2_core = state.storage[t2_argument].clone(); @@ -171,8 +171,8 @@ pub fn unify( where Q: ExternalQueries, { - let t1 = normalise::normalise_expand_type(state, context, t1)?; - let t2 = normalise::normalise_expand_type(state, context, t2)?; + let t1 = toolkit::normalise_expand_type(state, context, t1)?; + let t2 = toolkit::normalise_expand_type(state, context, t2)?; crate::debug_fields!(state, context, { t1 = t1, t2 = t2 }); From 0ecf0067007ffbcaea3af4d0e0e09d17f0b1451a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Feb 2026 22:48:29 +0800 Subject: [PATCH 154/386] Rename constraint collection functions --- compiler-core/checking/src/algorithm/binder.rs | 2 +- compiler-core/checking/src/algorithm/operator.rs | 6 +++--- compiler-core/checking/src/algorithm/term.rs | 11 +++++------ compiler-core/checking/src/algorithm/toolkit.rs | 6 +++--- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index dad5438a..d4a9f54d 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -142,7 +142,7 @@ where // Non-nullary constructors are instantiated during application. let inferred_type = if arguments.is_empty() { constructor_t = toolkit::instantiate_forall(state, constructor_t); - toolkit::collect_constraints(state, constructor_t) + toolkit::collect_wanteds(state, constructor_t) } else { for &argument in arguments.iter() { constructor_t = binder::check_constructor_binder_application( diff --git a/compiler-core/checking/src/algorithm/operator.rs b/compiler-core/checking/src/algorithm/operator.rs index f9faa40f..0d439e82 100644 --- a/compiler-core/checking/src/algorithm/operator.rs +++ b/compiler-core/checking/src/algorithm/operator.rs @@ -62,7 +62,7 @@ where // Peel constraints from the expected type as givens, // so operator arguments like `unsafePartial $ expr` // can discharge constraints like Partial properly. - let expected_type = toolkit::collect_given_constraints(state, expected_type); + let expected_type = toolkit::collect_givens(state, expected_type); E::check_surface(state, context, *type_id, expected_type) } }, @@ -112,7 +112,7 @@ where }; let operator_type = toolkit::instantiate_forall(state, operator_type); - let operator_type = toolkit::collect_constraints(state, operator_type); + let operator_type = toolkit::collect_wanteds(state, operator_type); let synthesise = toolkit::SynthesiseFunction::Yes; @@ -133,7 +133,7 @@ where if let OperatorKindMode::Check { expected_type } = mode { // Peel constraints from the expected type as givens, // so operator result constraints can be discharged. - let expected_type = toolkit::collect_given_constraints(state, expected_type); + let expected_type = toolkit::collect_givens(state, expected_type); let _ = unification::subtype(state, context, result_type, expected_type)?; } diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index deb62f38..0124e358 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -39,10 +39,9 @@ fn check_expression_quiet( where Q: ExternalQueries, { - // TODO: move normalisation to toolkit let expected = toolkit::normalise_expand_type(state, context, expected)?; let expected = toolkit::skolemise_forall(state, expected); - let expected = toolkit::collect_given_constraints(state, expected); + let expected = toolkit::collect_givens(state, expected); if let Some(section_result) = context.sectioned.expressions.get(&expression) { check_sectioned_expression(state, context, expression, section_result, expected) } else { @@ -625,7 +624,7 @@ where } else { let id = infer_expression(state, context, *value)?; let id = toolkit::instantiate_forall(state, id); - toolkit::collect_constraints(state, id) + toolkit::collect_wanteds(state, id) }; fields.push(RowField { label, id }); @@ -646,7 +645,7 @@ where id } else { let id = toolkit::instantiate_forall(state, id); - toolkit::collect_constraints(state, id) + toolkit::collect_wanteds(state, id) }; fields.push(RowField { label, id }); @@ -1306,7 +1305,7 @@ where // Instantiate to avoid polymorphic types in record fields. let id = toolkit::instantiate_forall(state, id); - let id = toolkit::collect_constraints(state, id); + let id = toolkit::collect_wanteds(state, id); fields.push(RowField { label, id }); } @@ -1319,7 +1318,7 @@ where // Instantiate to avoid polymorphic types in record fields. let id = toolkit::instantiate_forall(state, id); - let id = toolkit::collect_constraints(state, id); + let id = toolkit::collect_wanteds(state, id); fields.push(RowField { label, id }); } diff --git a/compiler-core/checking/src/algorithm/toolkit.rs b/compiler-core/checking/src/algorithm/toolkit.rs index 329cc798..31d110e4 100644 --- a/compiler-core/checking/src/algorithm/toolkit.rs +++ b/compiler-core/checking/src/algorithm/toolkit.rs @@ -135,7 +135,7 @@ pub fn skolemise_forall(state: &mut CheckState, mut type_id: TypeId) -> TypeId { } /// Collects [`Type::Constrained`] as wanted constraints. -pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeId { +pub fn collect_wanteds(state: &mut CheckState, mut type_id: TypeId) -> TypeId { safe_loop! { type_id = state.normalize_type(type_id); if let Type::Constrained(constraint, constrained) = state.storage[type_id] { @@ -153,7 +153,7 @@ pub fn collect_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeI /// a wanted. Used when the expected type carries constraints that should /// discharge wanted constraints from the inferred type e.g. `unsafePartial` /// discharging `Partial`. -pub fn collect_given_constraints(state: &mut CheckState, mut type_id: TypeId) -> TypeId { +pub fn collect_givens(state: &mut CheckState, mut type_id: TypeId) -> TypeId { safe_loop! { type_id = state.normalize_type(type_id); if let Type::Constrained(constraint, constrained) = state.storage[type_id] { @@ -168,7 +168,7 @@ pub fn collect_given_constraints(state: &mut CheckState, mut type_id: TypeId) -> /// [`instantiate_forall`] then [`collect_constraints`]. pub fn instantiate_constrained(state: &mut CheckState, type_id: TypeId) -> TypeId { let type_id = instantiate_forall(state, type_id); - collect_constraints(state, type_id) + collect_wanteds(state, type_id) } /// Instantiates [`Type::Forall`] with the provided arguments. From 92a5ee04cf4b803599a470b163a38a44139ccb10 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 00:13:54 +0800 Subject: [PATCH 155/386] Move manual span calls into instrument --- .../checking/src/algorithm/term_item.rs | 17 ++++++----------- .../checking/src/algorithm/type_item.rs | 10 ++++------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/compiler-core/checking/src/algorithm/term_item.rs b/compiler-core/checking/src/algorithm/term_item.rs index 149fefbd..edb68a64 100644 --- a/compiler-core/checking/src/algorithm/term_item.rs +++ b/compiler-core/checking/src/algorithm/term_item.rs @@ -30,6 +30,7 @@ pub struct InferredValueGroup { /// For [`TermItemIr::ValueGroup`] specifically, it also invokes the /// [`inspect::collect_signature_variables`] function to collect type /// variables that need to be rebound during [`check_value_group`]. +#[tracing::instrument(skip_all, name = "check_term_signature")] pub fn check_term_signature( state: &mut CheckState, context: &CheckContext, @@ -39,8 +40,6 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { - let _span = tracing::debug_span!("check_term_signature").entered(); - let Some(item) = context.lowered.info.get_term_item(item_id) else { return Ok(()); }; @@ -110,6 +109,7 @@ pub struct CheckInstance<'a> { /// upon completion. /// /// [`core::Instance`]: crate::core::Instance +#[tracing::instrument(skip_all, name = "check_instance")] pub fn check_instance( state: &mut CheckState, context: &CheckContext, @@ -120,8 +120,6 @@ where { let CheckInstance { item_id, constraints, arguments, resolution } = input; state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { - let _span = tracing::debug_span!("check_instance").entered(); - let Some((class_file, class_item)) = *resolution else { return Ok(()); }; @@ -275,6 +273,7 @@ pub struct CheckValueGroup<'a> { /// /// This function optionally returns [`InferredValueGroup`] /// for value declarations that do not have a signature. +#[tracing::instrument(skip_all, name = "check_value_group")] pub fn check_value_group( state: &mut CheckState, context: &CheckContext, @@ -284,10 +283,7 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TermDeclaration(input.item_id), |state| { - state.with_implication(|state| { - let _span = tracing::debug_span!("check_value_group").entered(); - check_value_group_core(context, state, input) - }) + state.with_implication(|state| check_value_group_core(context, state, input)) }) } @@ -360,6 +356,7 @@ where } /// Generalises an [`InferredValueGroup`]. +#[tracing::instrument(skip_all, name = "commit_inferred_value_group")] pub fn commit_inferred_value_group( state: &mut CheckState, context: &CheckContext, @@ -378,7 +375,6 @@ where }; state.with_error_step(ErrorStep::TermDeclaration(item_id), |state| { - let _span = tracing::debug_span!("commit_value_group").entered(); for constraint in result.ambiguous { let constraint = state.render_local_type(context, constraint); state.insert_error(ErrorKind::AmbiguousConstraint { constraint }); @@ -505,6 +501,7 @@ pub struct CheckInstanceMemberGroup<'a> { /// The signature type of a member group must unify with the specialised /// type of the class member. The signature cannot be more general than /// the specialised type. See tests 118 and 125 for a demonstration. +#[tracing::instrument(skip_all, name = "check_instance_member_group")] pub fn check_instance_member_group( state: &mut CheckState, context: &CheckContext, @@ -537,8 +534,6 @@ where .. } = input; - let _span = tracing::debug_span!("check_instance_member_group").entered(); - // Save the current size of the environment for unbinding. let size = state.type_scope.size(); diff --git a/compiler-core/checking/src/algorithm/type_item.rs b/compiler-core/checking/src/algorithm/type_item.rs index 56240b38..ae997cb2 100644 --- a/compiler-core/checking/src/algorithm/type_item.rs +++ b/compiler-core/checking/src/algorithm/type_item.rs @@ -64,6 +64,7 @@ pub enum CheckedTypeItem { /// - [`check_data_definition`] /// - [`check_synonym_definition`] /// - [`check_class_definition`] +#[tracing::instrument(skip_all, name = "check_type_item")] pub fn check_type_item( state: &mut CheckState, context: &CheckContext, @@ -73,8 +74,6 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TypeDeclaration(item_id), |state| { - let _span = tracing::debug_span!("check_type_item").entered(); - let Some(item) = context.lowered.info.get_type_item(item_id) else { return Ok(None); }; @@ -686,6 +685,7 @@ where /// To enable scoped type variables, this function also populates the /// [`CheckState::surface_bindings`] with the kind variables found in /// the signature. +#[tracing::instrument(skip_all, name = "check_type_signature")] pub fn check_type_signature( state: &mut CheckState, context: &CheckContext, @@ -695,8 +695,6 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::TypeDeclaration(item_id), |state| { - let _span = tracing::debug_span!("check_type_signature").entered(); - let Some(item) = context.lowered.info.get_type_item(item_id) else { return Ok(()); }; @@ -957,6 +955,7 @@ where Ok(()) } +#[tracing::instrument(skip_all, name = "check_constructor_arguments")] fn check_constructor_arguments( state: &mut CheckState, context: &CheckContext, @@ -965,7 +964,6 @@ fn check_constructor_arguments( where Q: ExternalQueries, { - let _span = tracing::debug_span!("check_constructor_arguments").entered(); let mut constructors = vec![]; for item_id in context.indexed.pairs.data_constructors(item_id) { @@ -986,6 +984,7 @@ where Ok(constructors) } +#[tracing::instrument(skip_all, name = "infer_constructor_argument")] fn infer_constructor_argument( state: &mut CheckState, context: &CheckContext, @@ -995,7 +994,6 @@ where Q: ExternalQueries, { state.with_error_step(ErrorStep::ConstructorArgument(argument), |state| { - let _span = tracing::debug_span!("infer_constructor_argument").entered(); let (inferred_type, _) = kind::check_surface_kind(state, context, argument, context.prim.t)?; Ok(inferred_type) From b571df2e82645856f5325cb7d39497f8d7536f6c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 00:19:24 +0800 Subject: [PATCH 156/386] Reorganise compiler-compatibility crates --- Cargo.lock | 22 +++---- Cargo.toml | 3 +- .../{ => command}/Cargo.toml | 8 +-- .../{ => command}/src/compat.rs | 0 .../{ => command}/src/error.rs | 2 +- compiler-compatibility/command/src/layout.rs | 34 ++++++++++ .../{ => command}/src/loader.rs | 0 .../{ => command}/src/main.rs | 23 +++---- .../command/src/repositories.rs | 50 ++++++++++++++ .../{ => command}/src/resolver.rs | 2 +- .../{ => command}/src/storage.rs | 2 +- .../{ => command}/src/trace.rs | 0 .../{ => command}/src/types.rs | 0 .../{ => command}/src/unpacker.rs | 0 .../registry}/Cargo.toml | 2 +- .../registry}/src/error.rs | 0 .../registry}/src/layout.rs | 0 .../registry}/src/lib.rs | 0 .../registry}/src/reader.rs | 0 .../registry}/src/types.rs | 0 compiler-compatibility/src/layout.rs | 36 ---------- compiler-compatibility/src/registry.rs | 65 ------------------- 22 files changed, 116 insertions(+), 133 deletions(-) rename compiler-compatibility/{ => command}/Cargo.toml (68%) rename compiler-compatibility/{ => command}/src/compat.rs (100%) rename compiler-compatibility/{ => command}/src/error.rs (93%) create mode 100644 compiler-compatibility/command/src/layout.rs rename compiler-compatibility/{ => command}/src/loader.rs (100%) rename compiler-compatibility/{ => command}/src/main.rs (86%) create mode 100644 compiler-compatibility/command/src/repositories.rs rename compiler-compatibility/{ => command}/src/resolver.rs (98%) rename compiler-compatibility/{ => command}/src/storage.rs (96%) rename compiler-compatibility/{ => command}/src/trace.rs (100%) rename compiler-compatibility/{ => command}/src/types.rs (100%) rename compiler-compatibility/{ => command}/src/unpacker.rs (100%) rename {purescript-registry => compiler-compatibility/registry}/Cargo.toml (86%) rename {purescript-registry => compiler-compatibility/registry}/src/error.rs (100%) rename {purescript-registry => compiler-compatibility/registry}/src/layout.rs (100%) rename {purescript-registry => compiler-compatibility/registry}/src/lib.rs (100%) rename {purescript-registry => compiler-compatibility/registry}/src/reader.rs (100%) rename {purescript-registry => compiler-compatibility/registry}/src/types.rs (100%) delete mode 100644 compiler-compatibility/src/layout.rs delete mode 100644 compiler-compatibility/src/registry.rs diff --git a/Cargo.lock b/Cargo.lock index 36036b5a..7a49d920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,7 +403,7 @@ dependencies = [ "glob", "hex", "line-index", - "purescript-registry", + "registry", "reqwest", "rowan", "semver", @@ -1884,16 +1884,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "purescript-registry" -version = "0.1.0" -dependencies = [ - "semver", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "quote" version = "1.0.42" @@ -1986,6 +1976,16 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "registry" +version = "0.1.0" +dependencies = [ + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "reqwest" version = "0.12.28" diff --git a/Cargo.toml b/Cargo.toml index def1a3f3..ebf1d661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ [workspace] members = [ "compiler-bin", - "compiler-compatibility", + "compiler-compatibility/*", "compiler-core/*", "compiler-lsp/*", "compiler-scripts", "docs/src/wasm", - "purescript-registry", "tests-integration", "tests-package-set", ] diff --git a/compiler-compatibility/Cargo.toml b/compiler-compatibility/command/Cargo.toml similarity index 68% rename from compiler-compatibility/Cargo.toml rename to compiler-compatibility/command/Cargo.toml index 9f74c6b7..5259ccac 100644 --- a/compiler-compatibility/Cargo.toml +++ b/compiler-compatibility/command/Cargo.toml @@ -4,10 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] -purescript-registry = { path = "../purescript-registry" } -analyzer = { path = "../compiler-lsp/analyzer" } -files = { path = "../compiler-core/files" } -diagnostics = { path = "../compiler-core/diagnostics" } +registry = { path = "../registry" } +analyzer = { path = "../../compiler-lsp/analyzer" } +files = { path = "../../compiler-core/files" } +diagnostics = { path = "../../compiler-core/diagnostics" } clap = { version = "4", features = ["derive"] } git2 = "0.20" reqwest = { version = "0.12", features = ["blocking"] } diff --git a/compiler-compatibility/src/compat.rs b/compiler-compatibility/command/src/compat.rs similarity index 100% rename from compiler-compatibility/src/compat.rs rename to compiler-compatibility/command/src/compat.rs diff --git a/compiler-compatibility/src/error.rs b/compiler-compatibility/command/src/error.rs similarity index 93% rename from compiler-compatibility/src/error.rs rename to compiler-compatibility/command/src/error.rs index 102e9f08..148a3168 100644 --- a/compiler-compatibility/src/error.rs +++ b/compiler-compatibility/command/src/error.rs @@ -15,7 +15,7 @@ pub enum CompatError { Semver(#[from] semver::Error), #[error("registry: {0}")] - Registry(#[from] purescript_registry::RegistryError), + Registry(#[from] registry::RegistryError), #[error("package set missing package: {0}")] MissingFromPackageSet(String), diff --git a/compiler-compatibility/command/src/layout.rs b/compiler-compatibility/command/src/layout.rs new file mode 100644 index 00000000..72141662 --- /dev/null +++ b/compiler-compatibility/command/src/layout.rs @@ -0,0 +1,34 @@ +use std::path::{Path, PathBuf}; + +use registry::RegistryLayout; + +/// Layout for compiler-compatibility tool directories. +/// +/// Includes paths for repository checkouts, tarball cache, and unpacked packages. +#[derive(Debug, Clone)] +pub struct Layout { + pub registry: RegistryLayout, + pub cache_tarballs: PathBuf, + pub packages: PathBuf, +} + +impl Layout { + pub fn new(root: impl AsRef) -> Layout { + let root = root.as_ref().to_path_buf(); + let repos_dir = root.join("repos"); + let registry_dir = repos_dir.join("registry"); + let index_dir = repos_dir.join("registry-index"); + let cache_tarballs = root.join("cache").join("tarballs"); + let packages = root.join("packages"); + + Layout { + registry: RegistryLayout::new(®istry_dir, &index_dir), + cache_tarballs, + packages, + } + } + + pub fn tarball_cache_path(&self, name: &str, version: &str) -> PathBuf { + self.cache_tarballs.join(format!("{}-{}.tar.gz", name, version)) + } +} diff --git a/compiler-compatibility/src/loader.rs b/compiler-compatibility/command/src/loader.rs similarity index 100% rename from compiler-compatibility/src/loader.rs rename to compiler-compatibility/command/src/loader.rs diff --git a/compiler-compatibility/src/main.rs b/compiler-compatibility/command/src/main.rs similarity index 86% rename from compiler-compatibility/src/main.rs rename to compiler-compatibility/command/src/main.rs index b29b19c7..1894ebf3 100644 --- a/compiler-compatibility/src/main.rs +++ b/compiler-compatibility/command/src/main.rs @@ -2,7 +2,7 @@ mod compat; mod error; mod layout; mod loader; -mod registry; +mod repositories; mod resolver; mod storage; mod trace; @@ -11,8 +11,8 @@ mod unpacker; use std::path::PathBuf; +use registry::{FsRegistry, RegistryReader}; use clap::Parser; -use purescript_registry::RegistryReader; use tracing::level_filters::LevelFilter; #[derive(Parser, Debug)] @@ -59,12 +59,13 @@ fn main() -> error::Result<()> { let tracing_handle = trace::init_tracing(stdout_level, cli.log_level, cli.trace_output); let layout = layout::Layout::new(&cli.output); - let registry = registry::Registry::new(layout.clone()); - registry.ensure_repos(cli.update)?; + repositories::ensure_repositories(&layout.registry, cli.update)?; + + let reader = FsRegistry::new(layout.registry.clone()); if cli.list_sets { - let sets = registry.reader().list_package_sets()?; + let sets = reader.list_package_sets()?; for set in sets { println!("{}", set); } @@ -76,26 +77,26 @@ fn main() -> error::Result<()> { return Ok(()); } - let package_set = registry.reader().read_package_set(cli.package_set.as_deref())?; + let package_set = reader.read_package_set(cli.package_set.as_deref())?; tracing::info!(target: "compiler_compatibility", version = %package_set.version, "Using package set"); - let resolved = resolver::resolve(&cli.packages, &package_set, registry.reader())?; + let resolved = resolver::resolve(&cli.packages, &package_set, &reader)?; tracing::info!(target: "compiler_compatibility", count = resolved.packages.len(), "Resolved packages"); for (name, version) in &resolved.packages { - let metadata = registry.reader().read_metadata(name)?; + let metadata = reader.read_metadata(name)?; let published = metadata.published.get(version).ok_or_else(|| { error::CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } })?; let tarball = storage::fetch_tarball(name, version, &layout, cli.no_cache)?; storage::verify_tarball(&tarball, &published.hash, name, version)?; - unpacker::unpack_tarball(&tarball, &layout.packages_dir)?; + unpacker::unpack_tarball(&tarball, &layout.packages)?; tracing::info!(target: "compiler_compatibility", name, version, "Unpacked"); } - tracing::info!(target: "compiler_compatibility", directory = %layout.packages_dir.display(), "Finished unpacking"); + tracing::info!(target: "compiler_compatibility", directory = %layout.packages.display(), "Finished unpacking"); for package in &cli.packages { let _span = @@ -106,7 +107,7 @@ fn main() -> error::Result<()> { tracing_handle.begin_package(package).expect("failed to start package trace capture"); let log_file = guard.path().to_path_buf(); - let result = compat::check_package(&layout.packages_dir, package); + let result = compat::check_package(&layout.packages, package); drop(guard); diff --git a/compiler-compatibility/command/src/repositories.rs b/compiler-compatibility/command/src/repositories.rs new file mode 100644 index 00000000..c05ca160 --- /dev/null +++ b/compiler-compatibility/command/src/repositories.rs @@ -0,0 +1,50 @@ +use std::fs; + +use git2::build::RepoBuilder; +use git2::{FetchOptions, Repository}; +use registry::RegistryLayout; + +use crate::error::Result; + +const REGISTRY_URL: &str = "https://github.com/purescript/registry"; +const REGISTRY_INDEX_URL: &str = "https://github.com/purescript/registry-index"; + +pub fn ensure_repositories(layout: &RegistryLayout, update: bool) -> Result<()> { + if let Some(parent) = layout.registry_dir.parent() { + fs::create_dir_all(parent)?; + } + + ensure_repository(REGISTRY_URL, &layout.registry_dir, update)?; + ensure_repository(REGISTRY_INDEX_URL, &layout.index_dir, update)?; + + Ok(()) +} + +fn ensure_repository(url: &str, path: &std::path::Path, update: bool) -> Result<()> { + if path.exists() { + if update { + let repo = Repository::open(path)?; + let mut remote = repo.find_remote("origin")?; + remote.fetch(&["master"], None, None)?; + + let fetch_head = repo.find_reference("FETCH_HEAD")?; + let commit = repo.reference_to_annotated_commit(&fetch_head)?; + let (analysis, _) = repo.merge_analysis(&[&commit])?; + + if analysis.is_fast_forward() || analysis.is_normal() { + let refname = "refs/heads/master"; + let mut reference = repo.find_reference(refname)?; + reference.set_target(commit.id(), "pull: fast-forward")?; + repo.set_head(refname)?; + repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; + } + } + } else { + let mut fetch_opts = FetchOptions::new(); + fetch_opts.depth(1); + + RepoBuilder::new().fetch_options(fetch_opts).clone(url, path)?; + } + + Ok(()) +} diff --git a/compiler-compatibility/src/resolver.rs b/compiler-compatibility/command/src/resolver.rs similarity index 98% rename from compiler-compatibility/src/resolver.rs rename to compiler-compatibility/command/src/resolver.rs index cf931afb..bbb04289 100644 --- a/compiler-compatibility/src/resolver.rs +++ b/compiler-compatibility/command/src/resolver.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashSet}; -use purescript_registry::{PackageSet, RegistryReader}; +use registry::{PackageSet, RegistryReader}; use semver::Version; use crate::error::{CompatError, Result}; diff --git a/compiler-compatibility/src/storage.rs b/compiler-compatibility/command/src/storage.rs similarity index 96% rename from compiler-compatibility/src/storage.rs rename to compiler-compatibility/command/src/storage.rs index 24a9df96..b92bd298 100644 --- a/compiler-compatibility/src/storage.rs +++ b/compiler-compatibility/command/src/storage.rs @@ -23,7 +23,7 @@ pub fn fetch_tarball( return Ok(tarball_path); } - fs::create_dir_all(&layout.cache_tarballs_dir)?; + fs::create_dir_all(&layout.cache_tarballs)?; let url = tarball_url(name, version); let response = reqwest::blocking::get(&url)?; diff --git a/compiler-compatibility/src/trace.rs b/compiler-compatibility/command/src/trace.rs similarity index 100% rename from compiler-compatibility/src/trace.rs rename to compiler-compatibility/command/src/trace.rs diff --git a/compiler-compatibility/src/types.rs b/compiler-compatibility/command/src/types.rs similarity index 100% rename from compiler-compatibility/src/types.rs rename to compiler-compatibility/command/src/types.rs diff --git a/compiler-compatibility/src/unpacker.rs b/compiler-compatibility/command/src/unpacker.rs similarity index 100% rename from compiler-compatibility/src/unpacker.rs rename to compiler-compatibility/command/src/unpacker.rs diff --git a/purescript-registry/Cargo.toml b/compiler-compatibility/registry/Cargo.toml similarity index 86% rename from purescript-registry/Cargo.toml rename to compiler-compatibility/registry/Cargo.toml index fd0a4e70..efa0d68f 100644 --- a/purescript-registry/Cargo.toml +++ b/compiler-compatibility/registry/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "purescript-registry" +name = "registry" version = "0.1.0" edition = "2024" diff --git a/purescript-registry/src/error.rs b/compiler-compatibility/registry/src/error.rs similarity index 100% rename from purescript-registry/src/error.rs rename to compiler-compatibility/registry/src/error.rs diff --git a/purescript-registry/src/layout.rs b/compiler-compatibility/registry/src/layout.rs similarity index 100% rename from purescript-registry/src/layout.rs rename to compiler-compatibility/registry/src/layout.rs diff --git a/purescript-registry/src/lib.rs b/compiler-compatibility/registry/src/lib.rs similarity index 100% rename from purescript-registry/src/lib.rs rename to compiler-compatibility/registry/src/lib.rs diff --git a/purescript-registry/src/reader.rs b/compiler-compatibility/registry/src/reader.rs similarity index 100% rename from purescript-registry/src/reader.rs rename to compiler-compatibility/registry/src/reader.rs diff --git a/purescript-registry/src/types.rs b/compiler-compatibility/registry/src/types.rs similarity index 100% rename from purescript-registry/src/types.rs rename to compiler-compatibility/registry/src/types.rs diff --git a/compiler-compatibility/src/layout.rs b/compiler-compatibility/src/layout.rs deleted file mode 100644 index 067e5df7..00000000 --- a/compiler-compatibility/src/layout.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::path::{Path, PathBuf}; - -use purescript_registry::RegistryLayout; - -/// Layout for compiler-compatibility tool directories. -/// -/// Includes paths for repository checkouts, tarball cache, and unpacked packages. -#[derive(Debug, Clone)] -pub struct Layout { - pub repos_dir: PathBuf, - pub registry_dir: PathBuf, - pub index_dir: PathBuf, - pub cache_tarballs_dir: PathBuf, - pub packages_dir: PathBuf, -} - -impl Layout { - pub fn new(root: impl AsRef) -> Layout { - let root = root.as_ref().to_path_buf(); - let repos_dir = root.join("repos"); - let registry_dir = repos_dir.join("registry"); - let index_dir = repos_dir.join("registry-index"); - let cache_tarballs_dir = root.join("cache").join("tarballs"); - let packages_dir = root.join("packages"); - - Layout { repos_dir, registry_dir, index_dir, cache_tarballs_dir, packages_dir } - } - - pub fn registry_layout(&self) -> RegistryLayout { - RegistryLayout::new(&self.registry_dir, &self.index_dir) - } - - pub fn tarball_cache_path(&self, name: &str, version: &str) -> PathBuf { - self.cache_tarballs_dir.join(format!("{}-{}.tar.gz", name, version)) - } -} diff --git a/compiler-compatibility/src/registry.rs b/compiler-compatibility/src/registry.rs deleted file mode 100644 index 01f72e11..00000000 --- a/compiler-compatibility/src/registry.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::fs; - -use git2::build::RepoBuilder; -use git2::{FetchOptions, Repository}; -use purescript_registry::FsRegistry; - -use crate::error::Result; -use crate::layout::Layout; - -const REGISTRY_URL: &str = "https://github.com/purescript/registry"; -const REGISTRY_INDEX_URL: &str = "https://github.com/purescript/registry-index"; - -pub struct Registry { - layout: Layout, - reader: FsRegistry, -} - -impl Registry { - pub fn new(layout: Layout) -> Registry { - let reader = FsRegistry::new(layout.registry_layout()); - Registry { layout, reader } - } - - pub fn ensure_repos(&self, update: bool) -> Result<()> { - fs::create_dir_all(&self.layout.repos_dir)?; - - self.ensure_repo(REGISTRY_URL, &self.layout.registry_dir, update)?; - self.ensure_repo(REGISTRY_INDEX_URL, &self.layout.index_dir, update)?; - - Ok(()) - } - - fn ensure_repo(&self, url: &str, path: &std::path::Path, update: bool) -> Result<()> { - if path.exists() { - if update { - let repo = Repository::open(path)?; - let mut remote = repo.find_remote("origin")?; - remote.fetch(&["master"], None, None)?; - - let fetch_head = repo.find_reference("FETCH_HEAD")?; - let commit = repo.reference_to_annotated_commit(&fetch_head)?; - let (analysis, _) = repo.merge_analysis(&[&commit])?; - - if analysis.is_fast_forward() || analysis.is_normal() { - let refname = "refs/heads/master"; - let mut reference = repo.find_reference(refname)?; - reference.set_target(commit.id(), "pull: fast-forward")?; - repo.set_head(refname)?; - repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; - } - } - } else { - let mut fetch_opts = FetchOptions::new(); - fetch_opts.depth(1); - - RepoBuilder::new().fetch_options(fetch_opts).clone(url, path)?; - } - - Ok(()) - } - - pub fn reader(&self) -> &FsRegistry { - &self.reader - } -} From c341ab3a49e99af8abadd4b0ee1ac9232cfb9528 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 00:31:48 +0800 Subject: [PATCH 157/386] Update API surface for check_package --- compiler-compatibility/command/src/compat.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 6c91d1d0..26ab5328 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -28,30 +28,30 @@ pub struct CheckResult { /// Checks all packages in the packages directory and returns diagnostics. /// -/// `packages_dir` should point to the directory containing unpacked packages. -/// `target_package` is the specific package to report diagnostics for (others are loaded as deps). -pub fn check_package(packages_dir: &Path, target_package: &str) -> CheckResult { +/// `packages` should point to the directory containing unpacked packages. +/// `target_package` is the specific package to report diagnostics for. +pub fn check_package(packages: &Path, target_package: &str) -> CheckResult { let _span = tracing::info_span!(target: "compiler_compatibility", "check_package", target_package) .entered(); - let (engine, files, file_ids) = loader::load_packages(packages_dir); + let (engine, files, file_ids) = loader::load_packages(packages); - let target_directory = find_package_dir(packages_dir, target_package); + let target_directory = find_package_dir(packages, target_package); let target_files = if let Some(directory) = &target_directory { loader::filter_package_files(&files, &file_ids, directory) } else { vec![] }; - let mut results = Vec::new(); + let mut results = vec![]; let mut total_errors = 0; let mut total_warnings = 0; tracing::info!(target: "compiler_compatibility", count = target_files.len()); for id in target_files { - let relative_path = compute_relative_path(&files, id, packages_dir); + let relative_path = compute_relative_path(&files, id, packages); let file_result = check_file(&engine, &files, id, &relative_path); total_errors += file_result.error_count; From e7137424a64d692d61c6b7df9186b59b8ef29099 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 02:35:28 +0800 Subject: [PATCH 158/386] Improve compiler-compatibility cli output --- Cargo.lock | 16 + compiler-compatibility/command/Cargo.toml | 4 + compiler-compatibility/command/src/compat.rs | 278 +++++++++++------- compiler-compatibility/command/src/main.rs | 4 +- .../command/src/resolver.rs | 116 +++++++- compiler-compatibility/command/src/trace.rs | 11 +- compiler-compatibility/command/src/types.rs | 2 + compiler-core/diagnostics/src/lib.rs | 2 +- compiler-core/diagnostics/src/render.rs | 39 ++- 9 files changed, 348 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a49d920..b7eb91ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ version = "0.1.0" dependencies = [ "analyzer", "base64", + "building-types", "clap", "diagnostics", "files", @@ -403,6 +404,8 @@ dependencies = [ "glob", "hex", "line-index", + "petgraph", + "rayon", "registry", "reqwest", "rowan", @@ -412,6 +415,7 @@ dependencies = [ "thiserror", "tracing", "tracing-subscriber", + "tracing-tree", "url", ] @@ -2835,6 +2839,18 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-tree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec" +dependencies = [ + "nu-ansi-term", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/compiler-compatibility/command/Cargo.toml b/compiler-compatibility/command/Cargo.toml index 5259ccac..e5af0b5a 100644 --- a/compiler-compatibility/command/Cargo.toml +++ b/compiler-compatibility/command/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] registry = { path = "../registry" } analyzer = { path = "../../compiler-lsp/analyzer" } +building-types = { path = "../../compiler-core/building-types" } files = { path = "../../compiler-core/files" } diagnostics = { path = "../../compiler-core/diagnostics" } clap = { version = "4", features = ["derive"] } @@ -23,4 +24,7 @@ base64 = "0.22.1" glob = "0.3" url = "2" line-index = "0.1" +petgraph = "0.8" +rayon = "1" rowan = "0.16" +tracing-tree = "0.4.1" diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 26ab5328..b708525b 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -1,16 +1,19 @@ //! Package compatibility checking with QueryEngine. +use std::collections::HashSet; use std::path::Path; use analyzer::QueryEngine; -use diagnostics::{DiagnosticsContext, Severity, ToDiagnostics}; +use building_types::QueryResult; +use diagnostics::{DiagnosticsContext, Severity, ToDiagnostics, format_rustc_with_path}; use files::{FileId, Files}; -use line_index::LineIndex; -use rowan::TextSize; - +use petgraph::graphmap::DiGraphMap; +use rayon::prelude::*; use url::Url; use crate::loader; +use crate::resolver; +use crate::types::ResolvedSet; /// Result of checking a single file. pub struct FileResult { @@ -26,17 +29,71 @@ pub struct CheckResult { pub total_warnings: usize, } +/// Primes dependency caches by processing files through the query pipeline in +/// topological order. Each layer is processed in parallel using rayon, so that +/// packages within the same layer are primed concurrently. +fn prime_dependencies( + engine: &QueryEngine, + files: &Files, + file_ids: &[FileId], + target_package: &str, + resolved: &ResolvedSet, + packages_dir: &Path, +) { + let layers = resolver::topological_order(resolved); + + for (layer_index, layer) in layers.iter().enumerate() { + let dependency_packages: Vec<&String> = + layer.iter().filter(|name| *name != target_package).collect(); + + if dependency_packages.is_empty() { + continue; + } + + let layer_files = dependency_packages.iter().flat_map(|package_name| { + let package_directory = find_package_dir(packages_dir, package_name); + if let Some(directory) = package_directory { + loader::filter_package_files(files, file_ids, &directory) + } else { + vec![] + } + }); + + let layer_files: Vec = layer_files.collect(); + let package_names: Vec<&str> = dependency_packages.iter().map(|s| s.as_str()).collect(); + + tracing::info!( + target: "compiler_compatibility", + layer = layer_index, + file_count = layer_files.len(), + packages = ?package_names, + "Processing" + ); + + // Prime each file in parallel through the full query pipeline. + layer_files.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.lowered(id); + let _ = snapshot.resolved(id); + let _ = snapshot.checked(id); + }); + } +} + /// Checks all packages in the packages directory and returns diagnostics. /// /// `packages` should point to the directory containing unpacked packages. /// `target_package` is the specific package to report diagnostics for. -pub fn check_package(packages: &Path, target_package: &str) -> CheckResult { +/// `resolved` provides the dependency graph for cache priming. +pub fn check_package(packages: &Path, target_package: &str, resolved: &ResolvedSet) -> CheckResult { let _span = tracing::info_span!(target: "compiler_compatibility", "check_package", target_package) .entered(); let (engine, files, file_ids) = loader::load_packages(packages); + prime_dependencies(&engine, &files, &file_ids, target_package, resolved, packages); + let target_directory = find_package_dir(packages, target_package); let target_files = if let Some(directory) = &target_directory { loader::filter_package_files(&files, &file_ids, directory) @@ -44,24 +101,81 @@ pub fn check_package(packages: &Path, target_package: &str) -> CheckResult { vec![] }; + target_files.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.indexed(id); + }); + + let layers = module_topological_layers(&engine, &target_files); + + tracing::info!( + target: "compiler_compatibility", + layer_count = layers.len(), + layers = ?layers.iter().map(|layer| layer.len()).collect::>(), + "Layers" + ); + + { + for (layer_index, layer) in layers.iter().enumerate() { + tracing::info!( + target: "compiler_compatibility", + layer = layer_index, + file_count = layer.len(), + "Processing" + ); + layer.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.lowered(id); + let _ = snapshot.resolved(id); + }); + } + } + let mut results = vec![]; let mut total_errors = 0; let mut total_warnings = 0; - tracing::info!(target: "compiler_compatibility", count = target_files.len()); + for layer in &layers { + for &id in layer { + let relative_path = compute_relative_path(&files, id, packages); + let file_result = collect_diagnostics(&engine, id, &relative_path); - for id in target_files { - let relative_path = compute_relative_path(&files, id, packages); - let file_result = check_file(&engine, &files, id, &relative_path); - - total_errors += file_result.error_count; - total_warnings += file_result.warning_count; - results.push(file_result); + total_errors += file_result.error_count; + total_warnings += file_result.warning_count; + results.push(file_result); + } } CheckResult { files: results, total_errors, total_warnings } } +/// Builds a module-level dependency graph from indexed imports and returns +/// files grouped into topological layers. +/// +/// Only tracks edges between files in the provided set. Files with no +/// intra-package dependencies go in layer 0. +fn module_topological_layers(engine: &QueryEngine, file_ids: &[FileId]) -> Vec> { + let file_set: HashSet = file_ids.iter().copied().collect(); + let mut graph = DiGraphMap::new(); + + for &id in file_ids { + graph.add_node(id); + } + + for &id in file_ids { + let Ok(indexed) = engine.indexed(id) else { continue }; + for import in indexed.imports.values() { + let Some(name) = &import.name else { continue }; + let Some(dependency_id) = engine.module_file(name) else { continue }; + if file_set.contains(&dependency_id) { + graph.add_edge(id, dependency_id, ()); + } + } + } + + resolver::topological_layers(&graph) +} + fn find_package_dir(packages_dir: &Path, package_name: &str) -> Option { let entries = std::fs::read_dir(packages_dir).ok()?; for entry in entries.filter_map(Result::ok) { @@ -87,19 +201,35 @@ fn compute_relative_path(files: &Files, id: FileId, base_dir: &Path) -> String { .unwrap_or_else(|| uri.to_string()) } -fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: &str) -> FileResult { - let _span = tracing::info_span!(target: "compiler_compatibility", "check_file").entered(); - - let content = engine.content(id); - let line_index = LineIndex::new(&content); +trait QueryResultExt { + fn or_file_error(self, path: &str, code: &str, message: &str) -> Result; +} - let Ok((parsed, _)) = engine.parsed(id) else { - return FileResult { +impl QueryResultExt for QueryResult { + fn or_file_error(self, path: &str, code: &str, message: &str) -> Result { + self.map_err(|_| FileResult { error_count: 1, warning_count: 0, - output: format!("{relative_path}:1:1: error[ParseError]: Failed to parse file\n"), - }; - }; + output: format!("{path}:1:1: error[{code}]: {message}\n"), + }) + } +} + +fn collect_diagnostics(engine: &QueryEngine, id: FileId, relative_path: &str) -> FileResult { + match collect_diagnostics_inner(engine, id, relative_path) { + Ok(result) | Err(result) => result, + } +} + +fn collect_diagnostics_inner( + engine: &QueryEngine, + id: FileId, + relative_path: &str, +) -> Result { + let content = engine.content(id); + + let (parsed, _) = + engine.parsed(id).or_file_error(relative_path, "ParseError", "Failed to parse file")?; if let Some(module_name) = parsed.module_name() { tracing::info!(target: "compiler_compatibility", module_name = %module_name); @@ -108,65 +238,31 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & }; let root = parsed.syntax_node(); - let stabilized = match engine.stabilized(id) { - Ok(s) => s, - Err(_) => { - return FileResult { - error_count: 1, - warning_count: 0, - output: format!( - "{relative_path}:1:1: error[StabilizeError]: Failed to stabilize\n" - ), - }; - } - }; - let indexed = match engine.indexed(id) { - Ok(i) => i, - Err(_) => { - return FileResult { - error_count: 1, - warning_count: 0, - output: format!("{relative_path}:1:1: error[IndexError]: Failed to index\n"), - }; - } - }; + let stabilized = engine.stabilized(id).or_file_error( + relative_path, + "StabilizeError", + "Failed to stabilize", + )?; + + let indexed = + engine.indexed(id).or_file_error(relative_path, "IndexError", "Failed to index")?; + + let lowered = + engine.lowered(id).or_file_error(relative_path, "LowerError", "Failed to lower")?; - let lowered = engine.lowered(id); let resolved = engine.resolved(id); - let checked = engine.checked(id); - - let lowered_ref = match &lowered { - Ok(l) => l, - Err(_) => { - return FileResult { - error_count: 1, - warning_count: 0, - output: format!("{relative_path}:1:1: error[LowerError]: Failed to lower\n"), - }; - } - }; - let checked_ref = match &checked { - Ok(c) => c, - Err(_) => { - return FileResult { - error_count: 1, - warning_count: 0, - output: format!("{relative_path}:1:1: error[CheckError]: Failed to check\n"), - }; - } - }; + let checked = + engine.checked(id).or_file_error(relative_path, "CheckError", "Failed to check")?; let context = - DiagnosticsContext::new(&content, &root, &stabilized, &indexed, lowered_ref, checked_ref); + DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &lowered, &checked); let mut all_diagnostics = vec![]; - if let Ok(ref lowered) = lowered { - for error in &lowered.errors { - all_diagnostics.extend(error.to_diagnostics(&context)); - } + for error in &lowered.errors { + all_diagnostics.extend(error.to_diagnostics(&context)); } if let Ok(ref resolved) = resolved { @@ -175,11 +271,10 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & } } - for error in &checked_ref.errors { + for error in &checked.errors { all_diagnostics.extend(error.to_diagnostics(&context)); } - let mut output = String::new(); let mut error_count = 0; let mut warning_count = 0; @@ -188,38 +283,9 @@ fn check_file(engine: &QueryEngine, _files: &Files, id: FileId, relative_path: & Severity::Error => error_count += 1, Severity::Warning => warning_count += 1, } - - let severity = match diagnostic.severity { - Severity::Error => "error", - Severity::Warning => "warning", - }; - - let offset = TextSize::from(diagnostic.primary.start); - let (line, col) = offset_to_line_col(&line_index, &content, offset); - - output.push_str(&format!( - "{relative_path}:{}:{}: {severity}[{}]: {}\n", - line + 1, - col + 1, - diagnostic.code, - diagnostic.message - )); } - FileResult { error_count, warning_count, output } -} - -fn offset_to_line_col(line_index: &LineIndex, content: &str, offset: TextSize) -> (u32, u32) { - let line_col = line_index.line_col(offset); - let line = line_col.line; - - let line_range = match line_index.line(line) { - Some(r) => r, - None => return (line, 0), - }; - let line_content = &content[line_range]; - let until_col = &line_content[..line_col.col as usize]; - let character = until_col.chars().count() as u32; + let output = format_rustc_with_path(&all_diagnostics, &content, relative_path); - (line, character) + Ok(FileResult { error_count, warning_count, output }) } diff --git a/compiler-compatibility/command/src/main.rs b/compiler-compatibility/command/src/main.rs index 1894ebf3..1561be22 100644 --- a/compiler-compatibility/command/src/main.rs +++ b/compiler-compatibility/command/src/main.rs @@ -11,8 +11,8 @@ mod unpacker; use std::path::PathBuf; -use registry::{FsRegistry, RegistryReader}; use clap::Parser; +use registry::{FsRegistry, RegistryReader}; use tracing::level_filters::LevelFilter; #[derive(Parser, Debug)] @@ -107,7 +107,7 @@ fn main() -> error::Result<()> { tracing_handle.begin_package(package).expect("failed to start package trace capture"); let log_file = guard.path().to_path_buf(); - let result = compat::check_package(&layout.packages, package); + let result = compat::check_package(&layout.packages, package, &resolved); drop(guard); diff --git a/compiler-compatibility/command/src/resolver.rs b/compiler-compatibility/command/src/resolver.rs index bbb04289..94486920 100644 --- a/compiler-compatibility/command/src/resolver.rs +++ b/compiler-compatibility/command/src/resolver.rs @@ -1,5 +1,6 @@ -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashSet, VecDeque}; +use petgraph::graphmap::DiGraphMap; use registry::{PackageSet, RegistryReader}; use semver::Version; @@ -12,6 +13,7 @@ pub fn resolve( registry: &impl RegistryReader, ) -> Result { let mut resolved = BTreeMap::new(); + let mut dependencies = BTreeMap::new(); let mut visited = HashSet::new(); for name in root_packages { @@ -20,10 +22,18 @@ pub fn resolve( .packages .get(name) .ok_or_else(|| CompatError::MissingFromPackageSet(name.clone()))?; - resolve_recursive(name, version, package_set, registry, &mut resolved, &mut visited)?; + resolve_recursive( + name, + version, + package_set, + registry, + &mut resolved, + &mut dependencies, + &mut visited, + )?; } - Ok(ResolvedSet { packages: resolved }) + Ok(ResolvedSet { packages: resolved, dependencies }) } fn resolve_recursive( @@ -32,6 +42,7 @@ fn resolve_recursive( package_set: &PackageSet, registry: &impl RegistryReader, resolved: &mut BTreeMap, + dependencies: &mut BTreeMap>, visited: &mut HashSet, ) -> Result<()> { if !visited.insert(name.to_string()) { @@ -46,6 +57,8 @@ fn resolve_recursive( CompatError::ManifestNotFound { name: name.to_string(), version: version.to_string() } })?; + let mut dependency_names = Vec::new(); + for (dep_name, range) in &manifest.dependencies { let dep_version = package_set .packages @@ -61,12 +74,107 @@ fn resolve_recursive( }); } - resolve_recursive(dep_name, dep_version, package_set, registry, resolved, visited)?; + dependency_names.push(dep_name.clone()); + resolve_recursive( + dep_name, + dep_version, + package_set, + registry, + resolved, + dependencies, + visited, + )?; } + dependencies.insert(name.to_string(), dependency_names); + Ok(()) } +/// Returns packages grouped into topological layers. +/// +/// Layer 0 contains packages with no dependencies, layer 1 contains packages +/// whose dependencies are all in layer 0, and so on. +pub fn topological_order(resolved: &ResolvedSet) -> Vec> { + let all_packages: HashSet<&String> = resolved.packages.keys().collect(); + + let mut graph = DiGraphMap::new(); + + for name in &all_packages { + graph.add_node(name.as_str()); + } + + // Edge direction: dependent -> dependency (name depends on dep). + for (name, deps) in &resolved.dependencies { + if all_packages.contains(name) { + for dep in deps { + if all_packages.contains(dep) { + graph.add_edge(name.as_str(), dep.as_str(), ()); + } + } + } + } + + topological_layers(&graph) + .into_iter() + .map(|layer| layer.into_iter().map(String::from).collect()) + .collect() +} + +/// Computes topological layers from a dependency graph using Kahn's algorithm. +/// +/// Edges point from dependent to dependency. Layer 0 contains nodes with no +/// outgoing edges (no dependencies), layer 1 contains nodes whose dependencies +/// are all in layer 0, and so on. +pub fn topological_layers( + graph: &DiGraphMap, +) -> Vec> { + // Count of unprocessed dependencies per node. + let mut dependency_count: BTreeMap = BTreeMap::new(); + for node in graph.nodes() { + dependency_count.insert(node, graph.neighbors(node).count()); + } + + // Reverse edges: dependency -> list of dependents. + let mut dependents: BTreeMap> = BTreeMap::new(); + for node in graph.nodes() { + for dependency in graph.neighbors(node) { + dependents.entry(dependency).or_default().push(node); + } + } + + let mut layers = Vec::new(); + let mut queue: VecDeque = VecDeque::new(); + + for (&node, &count) in &dependency_count { + if count == 0 { + queue.push_back(node); + } + } + + while !queue.is_empty() { + let layer: Vec = queue.drain(..).collect(); + let mut next_queue = VecDeque::new(); + + for &node in &layer { + if let Some(dependent_nodes) = dependents.get(&node) { + for &dependent in dependent_nodes { + let count = dependency_count.get_mut(&dependent).unwrap(); + *count -= 1; + if *count == 0 { + next_queue.push_back(dependent); + } + } + } + } + + layers.push(layer); + queue = next_queue; + } + + layers +} + fn version_satisfies_range(version: &str, range: &str) -> Result { let parsed_version: Version = version.parse()?; let parts: Vec<&str> = range.split_whitespace().collect(); diff --git a/compiler-compatibility/command/src/trace.rs b/compiler-compatibility/command/src/trace.rs index 0765ee57..8bb01efe 100644 --- a/compiler-compatibility/command/src/trace.rs +++ b/compiler-compatibility/command/src/trace.rs @@ -11,6 +11,7 @@ use tracing_subscriber::filter::{LevelFilter, Targets}; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; +use tracing_tree::HierarchicalLayer; struct RouterState { writer: Option>, @@ -135,10 +136,14 @@ pub fn init_tracing( ) -> TracingHandle { let router = CheckingLogsRouter::new(); - let stdout_filter = Targets::new() + let stderr_filter = Targets::new() .with_target("compiler_compatibility", stdout_level) .with_default(LevelFilter::OFF); - let stdout_layer = tracing_subscriber::fmt::layer().with_filter(stdout_filter); + let stderr_layer = HierarchicalLayer::new(2) + .with_writer(io::stderr) + .with_targets(true) + .with_indent_lines(true) + .with_filter(stderr_filter); let file_filter = Targets::new().with_target("checking", checking_level).with_default(LevelFilter::OFF); @@ -148,7 +153,7 @@ pub fn init_tracing( .with_span_events(FmtSpan::CLOSE) .with_filter(file_filter); - let subscriber = tracing_subscriber::registry().with(stdout_layer).with(file_layer); + let subscriber = tracing_subscriber::registry().with(stderr_layer).with(file_layer); tracing::subscriber::set_global_default(subscriber) .expect("failed to set global tracing subscriber"); diff --git a/compiler-compatibility/command/src/types.rs b/compiler-compatibility/command/src/types.rs index 9c300652..6b93f77c 100644 --- a/compiler-compatibility/command/src/types.rs +++ b/compiler-compatibility/command/src/types.rs @@ -1,4 +1,6 @@ #[derive(Debug, Clone, Default)] pub struct ResolvedSet { pub packages: std::collections::BTreeMap, + /// Maps each package name to its direct dependency package names. + pub dependencies: std::collections::BTreeMap>, } diff --git a/compiler-core/diagnostics/src/lib.rs b/compiler-core/diagnostics/src/lib.rs index 60c6470c..f1680563 100644 --- a/compiler-core/diagnostics/src/lib.rs +++ b/compiler-core/diagnostics/src/lib.rs @@ -6,4 +6,4 @@ mod render; pub use context::DiagnosticsContext; pub use convert::ToDiagnostics; pub use model::{Diagnostic, DiagnosticCode, RelatedSpan, Severity, Span}; -pub use render::{format_rustc, format_text, to_lsp_diagnostic}; +pub use render::{format_rustc, format_rustc_with_path, format_text, to_lsp_diagnostic}; diff --git a/compiler-core/diagnostics/src/render.rs b/compiler-core/diagnostics/src/render.rs index 480b8cd0..0d7e0413 100644 --- a/compiler-core/diagnostics/src/render.rs +++ b/compiler-core/diagnostics/src/render.rs @@ -62,6 +62,15 @@ fn span_location( } pub fn format_rustc(diagnostics: &[Diagnostic], content: &str) -> String { + format_rustc_inner(diagnostics, content, None) +} + +/// Renders diagnostics in rustc style with a file path in the `-->` lines. +pub fn format_rustc_with_path(diagnostics: &[Diagnostic], content: &str, path: &str) -> String { + format_rustc_inner(diagnostics, content, Some(path)) +} + +fn format_rustc_inner(diagnostics: &[Diagnostic], content: &str, path: Option<&str>) -> String { let line_index = LineIndex::new(content); let mut output = String::new(); @@ -81,10 +90,17 @@ pub fn format_rustc(diagnostics: &[Diagnostic], content: &str) -> String { let display_end_line = end_line + 1; let display_end_col = end_col + 1; - output.push_str(&format!( - " --> {}:{}..{}:{}\n", - display_start_line, display_start_col, display_end_line, display_end_col - )); + if let Some(path) = path { + output.push_str(&format!( + " --> {path}:{}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + } else { + output.push_str(&format!( + " --> {}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + } if let Some(line) = line_text(&line_index, content, start_line) { let line_num_width = display_start_line.to_string().len(); @@ -108,10 +124,17 @@ pub fn format_rustc(diagnostics: &[Diagnostic], content: &str) -> String { let display_end_line = end_line + 1; let display_end_col = end_col + 1; - output.push_str(&format!( - " --> {}:{}..{}:{}\n", - display_start_line, display_start_col, display_end_line, display_end_col - )); + if let Some(path) = path { + output.push_str(&format!( + " --> {path}:{}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + } else { + output.push_str(&format!( + " --> {}:{}..{}:{}\n", + display_start_line, display_start_col, display_end_line, display_end_col + )); + } if let Some(line) = line_text(&line_index, content, start_line) { let line_num_width = display_start_line.to_string().len(); From 5f803f89f2e95843f46285fb6964feb49c5d9927 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 03:02:41 +0800 Subject: [PATCH 159/386] Keep simple let bindings polymorphic --- compiler-core/checking/src/algorithm/term.rs | 15 ++- .../Main.snap | 6 +- .../326_let_retain_polymorphism/Main.purs | 28 ++++++ .../326_let_retain_polymorphism/Main.snap | 91 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.purs create mode 100644 tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.snap diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index 0124e358..aeae28ab 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -2060,10 +2060,19 @@ where state.type_scope.unbind_name(&variable.variable); } } else { - equation::infer_equations_core(state, context, name_type, &name.equations)?; + // Keep simple let bindings e.g. `bind = ibind` polymorphic. + if let [equation] = name.equations.as_ref() + && equation.binders.is_empty() + && let Some(guarded) = &equation.guarded + { + let inferred_type = infer_guarded_expression(state, context, guarded)?; + state.term_scope.bind_let(id, inferred_type); + } else { + equation::infer_equations_core(state, context, name_type, &name.equations)?; - let origin = equation::ExhaustivenessOrigin::FromType(name_type); - equation::patterns(state, context, origin, &name.equations)?; + let origin = equation::ExhaustivenessOrigin::FromType(name_type); + equation::patterns(state, context, origin, &name.equations)?; + } } Ok(()) diff --git a/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap b/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap index d4bf5280..f92f71c0 100644 --- a/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap +++ b/tests-integration/fixtures/checking/069_expression_sections_inference/Main.snap @@ -11,9 +11,9 @@ sub :: Int -> Int -> Int identity :: forall (a :: Type). (a :: Type) -> (a :: Type) test1 :: Int -> String -> { a :: Int, b :: String } test2 :: - forall (t13 :: Type) (t17 :: Type) (t18 :: Type). - ((((t18 :: Type) -> (t17 :: Type)) -> (t18 :: Type) -> (t17 :: Type)) -> (t13 :: Type)) -> - (t13 :: Type) + forall (t11 :: Type) (t15 :: Type) (t16 :: Type). + ((((t16 :: Type) -> (t15 :: Type)) -> (t16 :: Type) -> (t15 :: Type)) -> (t11 :: Type)) -> + (t11 :: Type) test3 :: Boolean -> Int -> Int test4 :: Int test5 :: { x :: Int, y :: Int | () } diff --git a/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.purs b/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.purs new file mode 100644 index 00000000..e248a6cb --- /dev/null +++ b/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.purs @@ -0,0 +1,28 @@ +module Main where + +class IxFunctor :: forall ix. (ix -> ix -> Type -> Type) -> Constraint +class IxFunctor f where + imap :: forall a b x y. (a -> b) -> f x y a -> f x y b + +class IxApply :: forall ix. (ix -> ix -> Type -> Type) -> Constraint +class IxFunctor m <= IxApply m where + iapply :: forall a b x y z. m x y (a -> b) -> m y z a -> m x z b + +class IxApplicative :: forall ix. (ix -> ix -> Type -> Type) -> Constraint +class IxApply m <= IxApplicative m where + ipure :: forall a x. a -> m x x a + +class IxBind :: forall ix. (ix -> ix -> Type -> Type) -> Constraint +class IxApply m <= IxBind m where + ibind :: forall a b x y z. m x y a -> (a -> m y z b) -> m x z b + +class IxMonad :: forall ix. (ix -> ix -> Type -> Type) -> Constraint +class (IxApplicative m, IxBind m) <= IxMonad m + +iap :: forall m a b x y z. IxMonad m => m x y (a -> b) -> m y z a -> m x z b +iap f a = do + f' <- f + a' <- a + ipure (f' a') + where + bind = ibind diff --git a/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.snap b/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.snap new file mode 100644 index 00000000..594e0328 --- /dev/null +++ b/tests-integration/fixtures/checking/326_let_retain_polymorphism/Main.snap @@ -0,0 +1,91 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +imap :: + forall (ix :: Type) (f :: (ix :: Type) -> (ix :: Type) -> Type -> Type) (a :: Type) (b :: Type) + (x :: (ix :: Type)) (y :: (ix :: Type)). + IxFunctor (f :: (ix :: Type) -> (ix :: Type) -> Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (y :: (ix :: Type)) + (a :: Type) -> + (f :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (y :: (ix :: Type)) + (b :: Type) +iapply :: + forall (ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) (a :: Type) (b :: Type) + (x :: (ix :: Type)) (y :: (ix :: Type)) (z :: (ix :: Type)). + IxApply (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) => + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (y :: (ix :: Type)) + ((a :: Type) -> (b :: Type)) -> + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (y :: (ix :: Type)) + (z :: (ix :: Type)) + (a :: Type) -> + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (z :: (ix :: Type)) + (b :: Type) +ipure :: + forall (ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) (a :: Type) + (x :: (ix :: Type)). + IxApplicative (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) => + (a :: Type) -> + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (x :: (ix :: Type)) + (a :: Type) +ibind :: + forall (ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) (a :: Type) (b :: Type) + (x :: (ix :: Type)) (y :: (ix :: Type)) (z :: (ix :: Type)). + IxBind (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) => + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (y :: (ix :: Type)) + (a :: Type) -> + ((a :: Type) -> + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (y :: (ix :: Type)) + (z :: (ix :: Type)) + (b :: Type)) -> + (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) + (x :: (ix :: Type)) + (z :: (ix :: Type)) + (b :: Type) +iap :: + forall (t33 :: Type) (m :: (t33 :: Type) -> (t33 :: Type) -> Type -> Type) (a :: Type) (b :: Type) + (x :: (t33 :: Type)) (y :: (t33 :: Type)) (z :: (t33 :: Type)). + IxMonad @(t33 :: Type) (m :: (t33 :: Type) -> (t33 :: Type) -> Type -> Type) => + (m :: (t33 :: Type) -> (t33 :: Type) -> Type -> Type) + (x :: (t33 :: Type)) + (y :: (t33 :: Type)) + ((a :: Type) -> (b :: Type)) -> + (m :: (t33 :: Type) -> (t33 :: Type) -> Type -> Type) + (y :: (t33 :: Type)) + (z :: (t33 :: Type)) + (a :: Type) -> + (m :: (t33 :: Type) -> (t33 :: Type) -> Type -> Type) + (x :: (t33 :: Type)) + (z :: (t33 :: Type)) + (b :: Type) + +Types +IxFunctor :: forall (ix :: Type). ((ix :: Type) -> (ix :: Type) -> Type -> Type) -> Constraint +IxApply :: forall (ix :: Type). ((ix :: Type) -> (ix :: Type) -> Type -> Type) -> Constraint +IxApplicative :: forall (ix :: Type). ((ix :: Type) -> (ix :: Type) -> Type -> Type) -> Constraint +IxBind :: forall (ix :: Type). ((ix :: Type) -> (ix :: Type) -> Type -> Type) -> Constraint +IxMonad :: forall (ix :: Type). ((ix :: Type) -> (ix :: Type) -> Type -> Type) -> Constraint + +Classes +class IxFunctor (f :: (ix :: Type) -> (ix :: Type) -> Type -> Type) +class IxFunctor @(ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) <= IxApply (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) +class IxApply @(ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) <= IxApplicative (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) +class IxApply @(ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) <= IxBind (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) +class IxApplicative @(ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type), IxBind @(ix :: Type) (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) <= IxMonad (m :: (ix :: Type) -> (ix :: Type) -> Type -> Type) diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 65600a9b..26a3f431 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -663,3 +663,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_324_foreign_kind_polymorphism_main() { run_test("324_foreign_kind_polymorphism", "Main"); } #[rustfmt::skip] #[test] fn test_325_type_kind_deferred_generalise_main() { run_test("325_type_kind_deferred_generalise", "Main"); } + +#[rustfmt::skip] #[test] fn test_326_let_retain_polymorphism_main() { run_test("326_let_retain_polymorphism", "Main"); } From a3c13fbf503128e9aae3cec3e5a531cb18cc5671 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 03:10:23 +0800 Subject: [PATCH 160/386] Clean up output for compiler-compatibility --- compiler-compatibility/command/src/compat.rs | 10 +++++----- compiler-compatibility/command/src/main.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index b708525b..7f9b5ff8 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -62,12 +62,12 @@ fn prime_dependencies( let layer_files: Vec = layer_files.collect(); let package_names: Vec<&str> = dependency_packages.iter().map(|s| s.as_str()).collect(); - tracing::info!( + tracing::debug!( target: "compiler_compatibility", layer = layer_index, file_count = layer_files.len(), packages = ?package_names, - "Processing" + "Priming" ); // Prime each file in parallel through the full query pipeline. @@ -108,7 +108,7 @@ pub fn check_package(packages: &Path, target_package: &str, resolved: &ResolvedS let layers = module_topological_layers(&engine, &target_files); - tracing::info!( + tracing::debug!( target: "compiler_compatibility", layer_count = layers.len(), layers = ?layers.iter().map(|layer| layer.len()).collect::>(), @@ -117,11 +117,11 @@ pub fn check_package(packages: &Path, target_package: &str, resolved: &ResolvedS { for (layer_index, layer) in layers.iter().enumerate() { - tracing::info!( + tracing::debug!( target: "compiler_compatibility", layer = layer_index, file_count = layer.len(), - "Processing" + "Priming" ); layer.par_iter().for_each(|&id| { let snapshot = engine.snapshot(); diff --git a/compiler-compatibility/command/src/main.rs b/compiler-compatibility/command/src/main.rs index 1561be22..0f464673 100644 --- a/compiler-compatibility/command/src/main.rs +++ b/compiler-compatibility/command/src/main.rs @@ -46,7 +46,7 @@ struct Cli { #[arg( long, value_name = "LevelFilter", - default_value = "debug", + default_value = "off", help = "Log level for checking crate traces" )] log_level: LevelFilter, @@ -93,7 +93,7 @@ fn main() -> error::Result<()> { storage::verify_tarball(&tarball, &published.hash, name, version)?; unpacker::unpack_tarball(&tarball, &layout.packages)?; - tracing::info!(target: "compiler_compatibility", name, version, "Unpacked"); + tracing::debug!(target: "compiler_compatibility", name, version, "Unpacked"); } tracing::info!(target: "compiler_compatibility", directory = %layout.packages.display(), "Finished unpacking"); From 07f5cf7eda3af17327b405cda16fc5b020b95123 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 13 Feb 2026 03:13:23 +0800 Subject: [PATCH 161/386] Add compatibility to justfile --- justfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index a84cbd71..64388957 100644 --- a/justfile +++ b/justfile @@ -26,11 +26,10 @@ coverage-html: [doc("Run integration tests with snapshot diffing: checking|lowering|resolving|lsp")] @t *args="": - cargo run -q -p compiler-scripts -- "$@" + cargo run -q -p compiler-scripts --release -- "$@" -[doc("Shorthand for 'just t checking'")] -@tc *args="": - cargo run -q -p compiler-scripts -- checking "$@" +@c *args="": + cargo run -q -p compiler-compatibility --release -- "$@" [doc("Apply clippy fixes and format")] fix: From e1244e11c380cc0d808a69c0559d9fdf2a75213a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 02:08:55 +0800 Subject: [PATCH 162/386] Fix missing trim for qualifier in qualified do/ado --- compiler-core/lowering/src/algorithm/recursive.rs | 4 ++-- .../fixtures/checking/327_qualified_do/Lib.purs | 5 +++++ .../fixtures/checking/327_qualified_do/Main.purs | 7 +++++++ .../fixtures/checking/327_qualified_do/Main.snap | 9 +++++++++ tests-integration/tests/checking/generated.rs | 2 ++ 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/327_qualified_do/Lib.purs create mode 100644 tests-integration/fixtures/checking/327_qualified_do/Main.purs create mode 100644 tests-integration/fixtures/checking/327_qualified_do/Main.snap diff --git a/compiler-core/lowering/src/algorithm/recursive.rs b/compiler-core/lowering/src/algorithm/recursive.rs index 357acdd7..021ca275 100644 --- a/compiler-core/lowering/src/algorithm/recursive.rs +++ b/compiler-core/lowering/src/algorithm/recursive.rs @@ -333,7 +333,7 @@ fn lower_expression_kind( cst::Expression::ExpressionDo(cst) => state.with_scope(|state| { let qualifier = cst.qualifier().and_then(|cst| { let token = cst.text()?; - let text = token.text(); + let text = token.text().trim_end_matches('.'); Some(SmolStr::from(text)) }); @@ -384,7 +384,7 @@ fn lower_expression_kind( cst::Expression::ExpressionAdo(cst) => state.with_scope(|state| { let qualifier = cst.qualifier().and_then(|cst| { let token = cst.text()?; - let text = token.text(); + let text = token.text().trim_end_matches('.'); Some(SmolStr::from(text)) }); diff --git a/tests-integration/fixtures/checking/327_qualified_do/Lib.purs b/tests-integration/fixtures/checking/327_qualified_do/Lib.purs new file mode 100644 index 00000000..d577cb29 --- /dev/null +++ b/tests-integration/fixtures/checking/327_qualified_do/Lib.purs @@ -0,0 +1,5 @@ +module Lib where + +foreign import bind :: forall m a b. m a -> (a -> m b) -> m b +foreign import discard :: forall m a b. m a -> (a -> m b) -> m b +foreign import pure :: forall m a. a -> m a diff --git a/tests-integration/fixtures/checking/327_qualified_do/Main.purs b/tests-integration/fixtures/checking/327_qualified_do/Main.purs new file mode 100644 index 00000000..fa02ac32 --- /dev/null +++ b/tests-integration/fixtures/checking/327_qualified_do/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Lib as L + +test = L.do + life <- L.pure 42 + L.pure life diff --git a/tests-integration/fixtures/checking/327_qualified_do/Main.snap b/tests-integration/fixtures/checking/327_qualified_do/Main.snap new file mode 100644 index 00000000..82c5b8f8 --- /dev/null +++ b/tests-integration/fixtures/checking/327_qualified_do/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: forall (t7 :: Type -> Type). (t7 :: Type -> Type) Int + +Types diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 26a3f431..091bc189 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -665,3 +665,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_325_type_kind_deferred_generalise_main() { run_test("325_type_kind_deferred_generalise", "Main"); } #[rustfmt::skip] #[test] fn test_326_let_retain_polymorphism_main() { run_test("326_let_retain_polymorphism", "Main"); } + +#[rustfmt::skip] #[test] fn test_327_qualified_do_main() { run_test("327_qualified_do", "Main"); } From 809c510cd9911e8faa1eea75a36ec9e963fbab15 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 02:24:09 +0800 Subject: [PATCH 163/386] Expand type synonyms on Coercible solving --- .../src/algorithm/constraint/compiler_solved/prim_coerce.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index 3ba0ab82..bdc22fa5 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -30,8 +30,8 @@ where return Ok(None); }; - let left = state.normalize_type(left); - let right = state.normalize_type(right); + let left = toolkit::normalise_expand_type(state, context, left)?; + let right = toolkit::normalise_expand_type(state, context, right)?; if left == right { return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); From 625f8f51012b14c07c264a92382a78ffd8b8fb99 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 02:25:57 +0800 Subject: [PATCH 164/386] Normalise tail-only rows into the tail --- compiler-core/checking/src/algorithm/state.rs | 7 +++++++ .../fixtures/checking/026_row_empty/Main.snap | 5 +++-- .../checking/233_record_instance_matching/Main.snap | 4 ++-- .../checking/234_record_instance_open_row/Main.snap | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking/src/algorithm/state.rs b/compiler-core/checking/src/algorithm/state.rs index e181b56e..3a4fc316 100644 --- a/compiler-core/checking/src/algorithm/state.rs +++ b/compiler-core/checking/src/algorithm/state.rs @@ -1287,6 +1287,13 @@ impl CheckState { break id; } } + Type::Row(ref row) if row.fields.is_empty() => { + if let Some(tail) = row.tail { + id = tail; + } else { + break id; + } + } _ => break id, } }; diff --git a/tests-integration/fixtures/checking/026_row_empty/Main.snap b/tests-integration/fixtures/checking/026_row_empty/Main.snap index b2b8902d..f554f0a3 100644 --- a/tests-integration/fixtures/checking/026_row_empty/Main.snap +++ b/tests-integration/fixtures/checking/026_row_empty/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -21,12 +22,12 @@ EmptyRecord = {} Kind = :0 Type = :0 -TailOnly = forall (t7 :: Type) (r :: Row (t7 :: Type)). ( | (r :: Row (t7 :: Type)) ) +TailOnly = forall (t7 :: Type) (r :: Row (t7 :: Type)). (r :: Row (t7 :: Type)) Quantified = :1 Kind = :0 Type = :1 -TailOnlyRecord = forall (r :: Row Type). { | (r :: Row Type) } +TailOnlyRecord = forall (r :: Row Type). {| (r :: Row Type) } Quantified = :0 Kind = :0 Type = :1 diff --git a/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap index 9f40bc18..991becc8 100644 --- a/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap +++ b/tests-integration/fixtures/checking/233_record_instance_matching/Main.snap @@ -19,7 +19,7 @@ class Make (a :: Type) (b :: Type) class Convert (a :: Type) (b :: Type) Instances -instance Make ({ | (r :: Row Type) } :: Type) ({ | (r :: Row Type) } :: Type) +instance Make ({| (r :: Row Type) } :: Type) ({| (r :: Row Type) } :: Type) chain: 0 -instance Convert ({ | (r :: Row Type) } :: Type) ({ converted :: { | (r :: Row Type) } } :: Type) +instance Convert ({| (r :: Row Type) } :: Type) ({ converted :: {| (r :: Row Type) } } :: Type) chain: 0 diff --git a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap index dc0138cb..a3dc99d0 100644 --- a/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap +++ b/tests-integration/fixtures/checking/234_record_instance_open_row/Main.snap @@ -20,7 +20,7 @@ class Clone (a :: Type) (b :: Type) class Nest (a :: Type) (b :: Type) Instances -instance Clone ({ | (r :: Row Type) } :: Type) ({ | (r :: Row Type) } :: Type) +instance Clone ({| (r :: Row Type) } :: Type) ({| (r :: Row Type) } :: Type) chain: 0 -instance Nest ({ | (r :: Row Type) } :: Type) ({ inner :: { | (r :: Row Type) }, outer :: Int } :: Type) +instance Nest ({| (r :: Row Type) } :: Type) ({ inner :: {| (r :: Row Type) }, outer :: Int } :: Type) chain: 0 From 8be1a1bf6f53496a2d564b124a08912a1b5076a0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 03:49:04 +0800 Subject: [PATCH 165/386] Implement structural equality checks for Coercible --- .../constraint/compiler_solved/prim_coerce.rs | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs index bdc22fa5..f7eec195 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_coerce.rs @@ -9,7 +9,7 @@ use crate::algorithm::constraint::{self, MatchInstance}; use crate::algorithm::safety::safe_loop; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::{derive, kind, substitute, toolkit}; -use crate::core::Role; +use crate::core::{Role, Variable}; use crate::{ExternalQueries, Type, TypeId}; enum NewtypeCoercionResult { @@ -41,6 +41,10 @@ where return Ok(Some(MatchInstance::Stuck)); } + if try_refl(state, context, left, right)? { + return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); + } + let newtype_result = try_newtype_coercion(state, context, left, right)?; if let NewtypeCoercionResult::Success(result) = newtype_result { return Ok(Some(result)); @@ -425,6 +429,119 @@ fn decompose_kind_for_coercion( } } +fn try_refl( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = toolkit::normalise_expand_type(state, context, t1)?; + let t2 = toolkit::normalise_expand_type(state, context, t2)?; + + if t1 == t2 { + return Ok(true); + } + + match (&state.storage[t1], &state.storage[t2]) { + ( + &Type::Application(t1_function, t1_argument), + &Type::Application(t2_function, t2_argument), + ) + | ( + &Type::Constrained(t1_function, t1_argument), + &Type::Constrained(t2_function, t2_argument), + ) + | ( + &Type::KindApplication(t1_function, t1_argument), + &Type::KindApplication(t2_function, t2_argument), + ) + | (&Type::Kinded(t1_function, t1_argument), &Type::Kinded(t2_function, t2_argument)) => { + Ok(try_refl(state, context, t1_function, t2_function)? + && try_refl(state, context, t1_argument, t2_argument)?) + } + + (&Type::Function(t1_argument, t1_result), &Type::Function(t2_argument, t2_result)) => { + Ok(try_refl(state, context, t1_argument, t2_argument)? + && try_refl(state, context, t1_result, t2_result)?) + } + + (&Type::Forall(ref t1_binder, t1_inner), &Type::Forall(ref t2_binder, t2_inner)) => { + Ok(try_refl(state, context, t1_binder.kind, t2_binder.kind)? + && try_refl(state, context, t1_inner, t2_inner)?) + } + + ( + &Type::OperatorApplication(t1_file, t1_item, t1_left, t1_right), + &Type::OperatorApplication(t2_file, t2_item, t2_left, t2_right), + ) => Ok((t1_file, t1_item) == (t2_file, t2_item) + && try_refl(state, context, t1_left, t2_left)? + && try_refl(state, context, t1_right, t2_right)?), + + ( + Type::SynonymApplication(_, t1_file, t1_item, t1_arguments), + Type::SynonymApplication(_, t2_file, t2_item, t2_arguments), + ) => { + let equal = (t1_file, t1_item) == (t2_file, t2_item) + && t1_arguments.len() == t2_arguments.len(); + + if !equal { + return Ok(false); + } + + let t1_arguments = Arc::clone(t1_arguments); + let t2_arguments = Arc::clone(t2_arguments); + + for (&t1_argument, &t2_argument) in + std::iter::zip(t1_arguments.iter(), t2_arguments.iter()) + { + if !try_refl(state, context, t1_argument, t2_argument)? { + return Ok(false); + } + } + + Ok(true) + } + + (Type::Row(t1_row), Type::Row(t2_row)) => { + if t1_row.fields.len() != t2_row.fields.len() { + return Ok(false); + } + + let t1_row = t1_row.clone(); + let t2_row = t2_row.clone(); + + for (t1_field, t2_field) in std::iter::zip(t1_row.fields.iter(), t2_row.fields.iter()) { + if t1_field.label != t2_field.label + || !try_refl(state, context, t1_field.id, t2_field.id)? + { + return Ok(false); + } + } + + match (t1_row.tail, t2_row.tail) { + (Some(t1_tail), Some(t2_tail)) => try_refl(state, context, t1_tail, t2_tail), + (None, None) => Ok(true), + _ => Ok(false), + } + } + + ( + &Type::Variable(Variable::Bound(ref t1_name, t1_kind)), + &Type::Variable(Variable::Bound(ref t2_name, t2_kind)), + ) => Ok(t1_name == t2_name && try_refl(state, context, t1_kind, t2_kind)?), + + ( + &Type::Variable(Variable::Skolem(ref t1_name, t1_kind)), + &Type::Variable(Variable::Skolem(ref t2_name, t2_kind)), + ) => Ok(t1_name == t2_name && try_refl(state, context, t1_kind, t2_kind)?), + + _ => Ok(false), + } +} + fn make_coercible_constraint( state: &mut CheckState, context: &CheckContext, From ed3b2d04764231b7c2b50a23c61df0554e4d2938 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 04:55:06 +0800 Subject: [PATCH 166/386] Implement conditional instantiation for pattern matching --- .../checking/src/algorithm/binder.rs | 22 ++++++++ compiler-core/checking/src/algorithm/term.rs | 43 ++++++++++++++- .../328_binder_instantiation/Main.purs | 25 +++++++++ .../328_binder_instantiation/Main.snap | 54 +++++++++++++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 tests-integration/fixtures/checking/328_binder_instantiation/Main.purs create mode 100644 tests-integration/fixtures/checking/328_binder_instantiation/Main.snap diff --git a/compiler-core/checking/src/algorithm/binder.rs b/compiler-core/checking/src/algorithm/binder.rs index d4a9f54d..81cecc84 100644 --- a/compiler-core/checking/src/algorithm/binder.rs +++ b/compiler-core/checking/src/algorithm/binder.rs @@ -60,6 +60,28 @@ where }) } +pub fn requires_instantiation(context: &CheckContext, binder_id: lowering::BinderId) -> bool +where + Q: ExternalQueries, +{ + let Some(kind) = context.lowered.info.get_binder_kind(binder_id) else { + return false; + }; + match kind { + lowering::BinderKind::Variable { .. } | lowering::BinderKind::Wildcard => false, + lowering::BinderKind::Named { binder, .. } => { + binder.is_some_and(|id| requires_instantiation(context, id)) + } + lowering::BinderKind::Parenthesized { parenthesized } => { + parenthesized.is_some_and(|id| requires_instantiation(context, id)) + } + lowering::BinderKind::Typed { binder, .. } => { + binder.is_some_and(|id| requires_instantiation(context, id)) + } + _ => true, + } +} + fn binder_core( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking/src/algorithm/term.rs b/compiler-core/checking/src/algorithm/term.rs index aeae28ab..dbadb91b 100644 --- a/compiler-core/checking/src/algorithm/term.rs +++ b/compiler-core/checking/src/algorithm/term.rs @@ -505,6 +505,25 @@ where Ok(expected) } +fn instantiate_trunk_types( + state: &mut CheckState, + context: &CheckContext, + trunk_types: &mut [TypeId], + branches: &[lowering::CaseBranch], +) where + Q: ExternalQueries, +{ + for (position, trunk_type) in trunk_types.iter_mut().enumerate() { + let should_instantiate = branches.iter().any(|branch| { + let binder = branch.binders.get(position); + binder.is_some_and(|&binder_id| binder::requires_instantiation(context, binder_id)) + }); + if should_instantiate { + *trunk_type = toolkit::instantiate_forall(state, *trunk_type); + } + } +} + fn check_case_of( state: &mut CheckState, context: &CheckContext, @@ -521,6 +540,8 @@ where trunk_types.push(trunk_type); } + instantiate_trunk_types(state, context, &mut trunk_types, branches); + for branch in branches.iter() { for (binder, trunk) in branch.binders.iter().zip(&trunk_types) { let _ = binder::check_binder(state, context, *binder, *trunk)?; @@ -687,6 +708,8 @@ where trunk_types.push(trunk_type); } + instantiate_trunk_types(state, context, &mut trunk_types, branches); + for branch in branches.iter() { for (binder, trunk) in branch.binders.iter().zip(&trunk_types) { let _ = binder::check_binder(state, context, *binder, *trunk)?; @@ -1963,11 +1986,27 @@ where let expression_type = infer_where_expression(state, context, where_expression)?; - let Some(binder) = binder else { + let Some(binder) = *binder else { return Ok(()); }; - let _ = binder::check_binder(state, context, *binder, expression_type)?; + let expression_type = if binder::requires_instantiation(context, binder) { + toolkit::instantiate_forall(state, expression_type) + } else { + expression_type + }; + + let binder_type = binder::check_binder(state, context, binder, expression_type)?; + + let exhaustiveness = + exhaustiveness::check_lambda_patterns(state, context, &[binder_type], &[binder])?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } Ok(()) } diff --git a/tests-integration/fixtures/checking/328_binder_instantiation/Main.purs b/tests-integration/fixtures/checking/328_binder_instantiation/Main.purs new file mode 100644 index 00000000..ceb764ab --- /dev/null +++ b/tests-integration/fixtures/checking/328_binder_instantiation/Main.purs @@ -0,0 +1,25 @@ +module Main where + +data Maybe a = Just a | Nothing +data Id = MkId (forall a. a -> a) + +identity :: forall a. Maybe (a -> a) +identity = Nothing + +test :: Partial => Int +test = case identity of + Just f -> let _ = f 42 in f true + +test2 :: Id -> Boolean +test2 x = case x of + MkId f -> let _ = f 42 in f true + +test3 :: Partial => Int +test3 = + let (Just f) = identity + in let _ = f 42 in f true + +test4 :: Id -> Boolean +test4 x = + let (MkId f) = x + in let _ = f 42 in f true diff --git a/tests-integration/fixtures/checking/328_binder_instantiation/Main.snap b/tests-integration/fixtures/checking/328_binder_instantiation/Main.snap new file mode 100644 index 00000000..baa27c7b --- /dev/null +++ b/tests-integration/fixtures/checking/328_binder_instantiation/Main.snap @@ -0,0 +1,54 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +MkId :: (forall (a :: Type). (a :: Type) -> (a :: Type)) -> Id +identity :: forall (a :: Type). Maybe ((a :: Type) -> (a :: Type)) +test :: Partial => Int +test2 :: Id -> Boolean +test3 :: Partial => Int +test4 :: Id -> Boolean + +Types +Maybe :: Type -> Type +Id :: Type + +Data +Maybe + Quantified = :0 + Kind = :0 + +Id + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] +Id = [] + +Diagnostics +error[CannotUnify]: Cannot unify 'Boolean' with 'Int' + --> 11:31..11:35 + | +11 | Just f -> let _ = f 42 in f true + | ^~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 10:8..11:35 + | +10 | test = case identity of + | ^~~~~~~~~~~~~~~~ +warning[MissingPatterns]: Pattern match is not exhaustive. Missing: Nothing + --> 19:3..20:28 + | +19 | let (Just f) = identity + | ^~~~~~~~~~~~~~~~~~~~~~~ +error[CannotUnify]: Cannot unify 'Boolean' with 'Int' + --> 20:24..20:28 + | +20 | in let _ = f 42 in f true + | ^~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 091bc189..c5919c9c 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -667,3 +667,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_326_let_retain_polymorphism_main() { run_test("326_let_retain_polymorphism", "Main"); } #[rustfmt::skip] #[test] fn test_327_qualified_do_main() { run_test("327_qualified_do", "Main"); } + +#[rustfmt::skip] #[test] fn test_328_binder_instantiation_main() { run_test("328_binder_instantiation", "Main"); } From 64b132d8861ae5171b0ddc40782c1fa0ee594776 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 14:22:49 +0800 Subject: [PATCH 167/386] Canonicalise record labels in exhaustiveness checking Record patterns that mention different subsets of a row type produced rows of unequal width in the specialisation matrix, causing a panic in default_matrix. Normalise all record constructors to the union of labels before specialising, padding missing labels with wildcard patterns. Co-Authored-By: Claude Opus 4.6 --- .../checking/src/algorithm/exhaustiveness.rs | 127 +++++++++++++++++- .../329_pattern_nothing_first/Main.purs | 8 ++ .../329_pattern_nothing_first/Main.snap | 30 +++++ .../330_record_subset_labels/Main.purs | 7 + .../330_record_subset_labels/Main.snap | 29 ++++ .../331_record_progressive_labels/Main.purs | 8 ++ .../331_record_progressive_labels/Main.snap | 31 +++++ .../332_record_equation_labels/Main.purs | 6 + .../332_record_equation_labels/Main.snap | 29 ++++ tests-integration/tests/checking/generated.rs | 8 ++ 10 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 tests-integration/fixtures/checking/329_pattern_nothing_first/Main.purs create mode 100644 tests-integration/fixtures/checking/329_pattern_nothing_first/Main.snap create mode 100644 tests-integration/fixtures/checking/330_record_subset_labels/Main.purs create mode 100644 tests-integration/fixtures/checking/330_record_subset_labels/Main.snap create mode 100644 tests-integration/fixtures/checking/331_record_progressive_labels/Main.purs create mode 100644 tests-integration/fixtures/checking/331_record_progressive_labels/Main.snap create mode 100644 tests-integration/fixtures/checking/332_record_equation_labels/Main.purs create mode 100644 tests-integration/fixtures/checking/332_record_equation_labels/Main.snap diff --git a/compiler-core/checking/src/algorithm/exhaustiveness.rs b/compiler-core/checking/src/algorithm/exhaustiveness.rs index 039cdb19..7bddc556 100644 --- a/compiler-core/checking/src/algorithm/exhaustiveness.rs +++ b/compiler-core/checking/src/algorithm/exhaustiveness.rs @@ -224,6 +224,7 @@ fn algorithm_u_constructor( where Q: ExternalQueries, { + let constructor = canonicalise_record_constructor(state, constructor, matrix); let specialised_matrix = specialise_matrix(state, &constructor, matrix); let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { @@ -376,6 +377,7 @@ fn algorithm_m_constructor( where Q: ExternalQueries, { + let constructor = canonicalise_record_constructor(state, constructor, matrix); let arity = constructor.arity(); let specialised_matrix = specialise_matrix(state, &constructor, matrix); @@ -522,6 +524,63 @@ where Ok(Some(witness)) } +/// Computes a canonical [`PatternConstructor::Record`] that includes the union of all +/// record labels appearing in the first column of the matrix and the given constructor. +/// +/// Record patterns in PureScript can mention different subsets of the full row type. +/// For example, `{ a }` and `{ a, b }` both match `{ a :: Int, b :: Int }`, but they +/// produce record constructors with different arities. The specialisation algorithm +/// requires all rows to have the same width, so we normalise to a canonical label set +/// before specialising. +/// +/// For non-record constructors this function returns the constructor unchanged. +fn canonicalise_record_constructor( + state: &CheckState, + constructor: PatternConstructor, + matrix: &PatternMatrix, +) -> PatternConstructor { + let PatternConstructor::Record { labels, fields } = &constructor else { + return constructor; + }; + + let mut canonical = { + let labels = labels.iter().cloned(); + let fields = fields.iter().copied(); + labels.zip(fields).collect_vec() + }; + + let initial_length = canonical.len(); + + for row in matrix { + let Some(&first) = row.first() else { + continue; + }; + + let pattern = &state.patterns[first]; + if let PatternKind::Constructor { + constructor: PatternConstructor::Record { labels, fields }, + } = &pattern.kind + { + for (label, &field) in iter::zip(labels, fields) { + if !canonical.iter().any(|(existing, _)| existing == label) { + let label = SmolStr::clone(label); + canonical.push((label, field)); + } + } + } + } + + if canonical.len() == initial_length { + return constructor; + } + + // Subtle: stable sorting by label + canonical.sort_by(|(a, _), (b, _)| a.cmp(b)); + + let (labels, fields) = canonical.into_iter().unzip(); + PatternConstructor::Record { labels, fields } +} + /// Specialises a [`PatternMatrix`] given a [`PatternConstructor`]. /// /// See documentation below for [`specialise_vector`]. @@ -550,13 +609,15 @@ fn specialise_vector( expected: &PatternConstructor, vector: &PatternVector, ) -> Option { - let [first_column, ref tail_columns @ ..] = vector[..] else { + let [first_column_id, ref tail_columns @ ..] = vector[..] else { unreachable!("invariant violated: specialise_vector processed empty row"); }; - let first_column = &state.patterns[first_column]; + // Clone to release any borrow on state.patterns, allowing mutable + // access later when allocating wildcard patterns for record padding. + let first_pattern = state.patterns[first_column_id].clone(); - if let PatternKind::Wildcard = first_column.kind { + if let PatternKind::Wildcard = first_pattern.kind { // Expand wildcard to the expected constructor's arity match expected { PatternConstructor::DataConstructor { fields, .. } @@ -582,7 +643,7 @@ fn specialise_vector( } } - let PatternKind::Constructor { constructor } = &first_column.kind else { + let PatternKind::Constructor { constructor } = &first_pattern.kind else { return Some(tail_columns.to_vec()); }; @@ -593,10 +654,12 @@ fn specialise_vector( // Splat fields for constructors with arity match constructor { - PatternConstructor::DataConstructor { fields, .. } - | PatternConstructor::Record { fields, .. } => { + PatternConstructor::DataConstructor { fields, .. } => { Some(iter::chain(fields, tail_columns).copied().collect()) } + PatternConstructor::Record { labels: actual_labels, fields: actual_fields } => { + specialise_record_fields(state, expected, actual_labels, actual_fields, tail_columns) + } PatternConstructor::Array { fields } => { Some(iter::chain(fields, tail_columns).copied().collect()) } @@ -604,6 +667,47 @@ fn specialise_vector( } } +/// Maps the fields of an actual record pattern to the expected (canonical) label set. +/// +/// When record patterns in different branches mention different subsets of labels, +/// the actual pattern may have fewer labels than the expected canonical constructor. +/// This function aligns the actual fields to the expected label positions, inserting +/// wildcard patterns for any labels present in the expected set but absent from the +/// actual pattern. +fn specialise_record_fields( + state: &mut CheckState, + expected: &PatternConstructor, + actual_labels: &[SmolStr], + actual_fields: &[PatternId], + tail_columns: &[PatternId], +) -> Option { + let PatternConstructor::Record { labels: expected_labels, fields: expected_fields } = expected + else { + return Some( + iter::chain(actual_fields.iter().copied(), tail_columns.iter().copied()).collect(), + ); + }; + + // Fast path: labels match exactly. + if actual_labels == expected_labels.as_slice() { + return Some( + iter::chain(actual_fields.iter().copied(), tail_columns.iter().copied()).collect(), + ); + } + + let mut mapped_fields = Vec::with_capacity(expected_labels.len()); + for (expected_label, &expected_field) in expected_labels.iter().zip(expected_fields.iter()) { + if let Some(position) = actual_labels.iter().position(|label| label == expected_label) { + mapped_fields.push(actual_fields[position]); + } else { + let t = state.patterns[expected_field].t; + mapped_fields.push(state.allocate_wildcard(t)); + } + } + + Some(mapped_fields.into_iter().chain(tail_columns.iter().copied()).collect()) +} + fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { let filter_map = matrix.iter().filter_map(|row| { let [first_column, ref default_columns @ ..] = row[..] else { @@ -698,6 +802,17 @@ where } } + // Canonicalise record constructors to include all labels from the matrix. + // Different record patterns may mention different subsets of the row type, + // and the specialisation algorithm requires a consistent arity. + if let Some(index) = + constructors.iter().position(|c| matches!(c, PatternConstructor::Record { .. })) + { + let record = constructors.remove(index); + let canonical = canonicalise_record_constructor(state, record, matrix); + constructors.insert(index, canonical); + } + let missing = collect_missing_constructors(state, context, scrutinee_type, &constructors)?; Ok(Sigma { constructors, missing }) } diff --git a/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.purs b/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.purs new file mode 100644 index 00000000..8a06720e --- /dev/null +++ b/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.purs @@ -0,0 +1,8 @@ +module Main where + +data Maybe a = Just a | Nothing + +test x = case x of + Nothing -> 0 + Just { a } -> 0 + Just { a, b } -> 0 diff --git a/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.snap b/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.snap new file mode 100644 index 00000000..d8a2aabe --- /dev/null +++ b/tests-integration/fixtures/checking/329_pattern_nothing_first/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: + forall (t7 :: Type) (t11 :: Type) (t13 :: Row Type). + Maybe { a :: (t7 :: Type) | ( b :: (t11 :: Type) | (t13 :: Row Type) ) } -> Int + +Types +Maybe :: Type -> Type + +Data +Maybe + Quantified = :0 + Kind = :0 + + +Roles +Maybe = [Representational] + +Diagnostics +warning[RedundantPattern]: Pattern match has redundant patterns: Just ({ a: _, b: _ }) + --> 5:10..8:21 + | +5 | test x = case x of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/330_record_subset_labels/Main.purs b/tests-integration/fixtures/checking/330_record_subset_labels/Main.purs new file mode 100644 index 00000000..6204d428 --- /dev/null +++ b/tests-integration/fixtures/checking/330_record_subset_labels/Main.purs @@ -0,0 +1,7 @@ +module Main where + +data Box a = Box a + +test x = case x of + Box { a, b } -> 0 + Box { a } -> 0 diff --git a/tests-integration/fixtures/checking/330_record_subset_labels/Main.snap b/tests-integration/fixtures/checking/330_record_subset_labels/Main.snap new file mode 100644 index 00000000..8ba2aee1 --- /dev/null +++ b/tests-integration/fixtures/checking/330_record_subset_labels/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) +test :: + forall (t6 :: Type) (t7 :: Type) (t12 :: Row Type). + Box { a :: (t6 :: Type), b :: (t7 :: Type) | (t12 :: Row Type) } -> Int + +Types +Box :: Type -> Type + +Data +Box + Quantified = :0 + Kind = :0 + + +Roles +Box = [Representational] + +Diagnostics +warning[RedundantPattern]: Pattern match has redundant patterns: Box ({ a: _ }) + --> 5:10..7:17 + | +5 | test x = case x of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/331_record_progressive_labels/Main.purs b/tests-integration/fixtures/checking/331_record_progressive_labels/Main.purs new file mode 100644 index 00000000..1338f10b --- /dev/null +++ b/tests-integration/fixtures/checking/331_record_progressive_labels/Main.purs @@ -0,0 +1,8 @@ +module Main where + +data Box a = Box a + +test x = case x of + Box { a } -> 0 + Box { a, b } -> 0 + Box { a, b, c } -> 0 diff --git a/tests-integration/fixtures/checking/331_record_progressive_labels/Main.snap b/tests-integration/fixtures/checking/331_record_progressive_labels/Main.snap new file mode 100644 index 00000000..c88b4405 --- /dev/null +++ b/tests-integration/fixtures/checking/331_record_progressive_labels/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) +test :: + forall (t6 :: Type) (t16 :: Type) (t17 :: Type) (t20 :: Row Type). + Box + { a :: (t6 :: Type) | ( b :: (t16 :: Type) | ( c :: (t17 :: Type) | (t20 :: Row Type) ) ) } -> + Int + +Types +Box :: Type -> Type + +Data +Box + Quantified = :0 + Kind = :0 + + +Roles +Box = [Representational] + +Diagnostics +warning[RedundantPattern]: Pattern match has redundant patterns: Box ({ a: _, b: _ }), Box ({ a: _, b: _, c: _ }) + --> 5:10..8:23 + | +5 | test x = case x of + | ^~~~~~~~~ diff --git a/tests-integration/fixtures/checking/332_record_equation_labels/Main.purs b/tests-integration/fixtures/checking/332_record_equation_labels/Main.purs new file mode 100644 index 00000000..75e44703 --- /dev/null +++ b/tests-integration/fixtures/checking/332_record_equation_labels/Main.purs @@ -0,0 +1,6 @@ +module Main where + +data Box a = Box a + +test (Box { a }) = 0 +test (Box { a, b }) = 0 diff --git a/tests-integration/fixtures/checking/332_record_equation_labels/Main.snap b/tests-integration/fixtures/checking/332_record_equation_labels/Main.snap new file mode 100644 index 00000000..e9b0503b --- /dev/null +++ b/tests-integration/fixtures/checking/332_record_equation_labels/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) +test :: + forall (t7 :: Type) (t8 :: Type) (t11 :: Row Type). + Box { a :: (t7 :: Type) | ( b :: (t8 :: Type) | (t11 :: Row Type) ) } -> Int + +Types +Box :: Type -> Type + +Data +Box + Quantified = :0 + Kind = :0 + + +Roles +Box = [Representational] + +Diagnostics +warning[RedundantPattern]: Pattern match has redundant patterns: Box ({ a: _, b: _ }) + --> 5:1..5:21 + | +5 | test (Box { a }) = 0 + | ^~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index c5919c9c..b9a170c6 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -669,3 +669,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_327_qualified_do_main() { run_test("327_qualified_do", "Main"); } #[rustfmt::skip] #[test] fn test_328_binder_instantiation_main() { run_test("328_binder_instantiation", "Main"); } + +#[rustfmt::skip] #[test] fn test_329_pattern_nothing_first_main() { run_test("329_pattern_nothing_first", "Main"); } + +#[rustfmt::skip] #[test] fn test_330_record_subset_labels_main() { run_test("330_record_subset_labels", "Main"); } + +#[rustfmt::skip] #[test] fn test_331_record_progressive_labels_main() { run_test("331_record_progressive_labels", "Main"); } + +#[rustfmt::skip] #[test] fn test_332_record_equation_labels_main() { run_test("332_record_equation_labels", "Main"); } From 3258547907019f99f65fa4ec17985c8ae1816e85 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 14:28:48 +0800 Subject: [PATCH 168/386] Implement full package set checking and reports --- Cargo.lock | 85 ++++++ compiler-compatibility/command/Cargo.toml | 3 + compiler-compatibility/command/src/compat.rs | 251 +++++++++++++++++- compiler-compatibility/command/src/error.rs | 3 - compiler-compatibility/command/src/main.rs | 153 ++++++++++- compiler-compatibility/command/src/report.rs | 39 +++ .../command/src/resolver.rs | 76 +++--- compiler-compatibility/registry/src/types.rs | 3 +- 8 files changed, 559 insertions(+), 54 deletions(-) create mode 100644 compiler-compatibility/command/src/report.rs diff --git a/Cargo.lock b/Cargo.lock index b7eb91ed..8850f009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,15 @@ dependencies = [ "url", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -316,6 +325,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -396,6 +419,7 @@ dependencies = [ "analyzer", "base64", "building-types", + "chrono", "clap", "diagnostics", "files", @@ -410,6 +434,8 @@ dependencies = [ "reqwest", "rowan", "semver", + "serde", + "serde_json", "sha2", "tar", "thiserror", @@ -1107,6 +1133,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -3088,6 +3138,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/compiler-compatibility/command/Cargo.toml b/compiler-compatibility/command/Cargo.toml index e5af0b5a..3a035711 100644 --- a/compiler-compatibility/command/Cargo.toml +++ b/compiler-compatibility/command/Cargo.toml @@ -28,3 +28,6 @@ petgraph = "0.8" rayon = "1" rowan = "0.16" tracing-tree = "0.4.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 7f9b5ff8..91c80711 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -1,6 +1,6 @@ //! Package compatibility checking with QueryEngine. -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use std::path::Path; use analyzer::QueryEngine; @@ -29,6 +29,22 @@ pub struct CheckResult { pub total_warnings: usize, } +/// Outcome for a single package in `check_all` mode. +pub struct PackageOutcome { + pub version: String, + pub total_errors: usize, + pub total_warnings: usize, + pub topo_layer: usize, + pub root_cause: bool, + pub cascaded_from: Vec, + pub cascaded_from_root_causes: Vec, +} + +/// Result of checking all packages. +pub struct AllCheckResult { + pub outcomes: BTreeMap, +} + /// Primes dependency caches by processing files through the query pipeline in /// topological order. Each layer is processed in parallel using rayon, so that /// packages within the same layer are primed concurrently. @@ -149,6 +165,229 @@ pub fn check_package(packages: &Path, target_package: &str, resolved: &ResolvedS CheckResult { files: results, total_errors, total_warnings } } +/// Checks a single package using an already-loaded engine. +/// +/// Files for the package must already be loaded in the engine. The caller +/// is responsible for ensuring dependencies are already cached via +/// topological ordering. +fn check_loaded_package( + engine: &QueryEngine, + files: &Files, + package_files: &[FileId], + packages_dir: &Path, + _quiet: bool, +) -> CheckResult { + // Index target files + package_files.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.indexed(id); + }); + + // Compute module-level topological layers within the package + let layers = module_topological_layers(engine, package_files); + + tracing::debug!( + target: "compiler_compatibility", + layer_count = layers.len(), + layers = ?layers.iter().map(|layer| layer.len()).collect::>(), + "Module layers" + ); + + // Prime in module-layer order + for layer in &layers { + layer.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.lowered(id); + let _ = snapshot.resolved(id); + }); + } + + // Collect diagnostics + let mut results = vec![]; + let mut total_errors = 0; + let mut total_warnings = 0; + + for layer in &layers { + for &id in layer { + let relative_path = compute_relative_path(files, id, packages_dir); + let file_result = collect_diagnostics(engine, id, &relative_path); + + total_errors += file_result.error_count; + total_warnings += file_result.warning_count; + results.push(file_result); + } + } + + CheckResult { files: results, total_errors, total_warnings } +} + +/// Checks all packages using a single shared engine, processing in +/// topological order to maximize cache reuse. +pub fn check_all(packages_dir: &Path, resolved: &ResolvedSet, quiet: bool) -> AllCheckResult { + let _span = tracing::info_span!(target: "compiler_compatibility", "check_all").entered(); + + // Load all files once + let (engine, files, file_ids) = loader::load_packages(packages_dir); + + // Build package -> files map + let mut package_files: BTreeMap> = BTreeMap::new(); + for name in resolved.packages.keys() { + let dir = find_package_dir(packages_dir, name); + let pkg_files = if let Some(directory) = dir { + loader::filter_package_files(&files, &file_ids, &directory) + } else { + tracing::warn!(target: "compiler_compatibility", package = name, "Package directory not found"); + vec![] + }; + package_files.insert(name.clone(), pkg_files); + } + + // Index all files upfront + tracing::info!(target: "compiler_compatibility", file_count = file_ids.len(), "Indexing all files"); + file_ids.par_iter().for_each(|&id| { + let snapshot = engine.snapshot(); + let _ = snapshot.indexed(id); + }); + + // Process in topological layers + let layers = resolver::topological_order(resolved); + let mut outcomes: BTreeMap = BTreeMap::new(); + + for (layer_index, layer) in layers.iter().enumerate() { + tracing::info!( + target: "compiler_compatibility", + layer = layer_index, + package_count = layer.len(), + "Processing layer" + ); + + // Check each package in the layer + // (packages within a layer have no mutual dependencies) + for package_name in layer { + let _pkg_span = tracing::info_span!( + target: "compiler_compatibility", + "check_package", + package = package_name + ) + .entered(); + + let pkg_files = + package_files.get(package_name).map(|v| v.as_slice()).unwrap_or(&[]); + let version = + resolved.packages.get(package_name).cloned().unwrap_or_default(); + + let result = if pkg_files.is_empty() { + // Synthetic error for missing package directory + CheckResult { + files: vec![FileResult { + error_count: 1, + warning_count: 0, + output: format!( + "{}: error: package directory not found\n", + package_name + ), + }], + total_errors: 1, + total_warnings: 0, + } + } else { + check_loaded_package(&engine, &files, pkg_files, packages_dir, quiet) + }; + + // Print diagnostics if not quiet + if !quiet { + for file_result in &result.files { + if !file_result.output.is_empty() { + print!("{}", file_result.output); + } + } + } + + // Log summary + let summary = format!( + "{}: {} errors, {} warnings", + package_name, result.total_errors, result.total_warnings + ); + if result.total_errors > 0 { + tracing::error!(target: "compiler_compatibility", "{}", summary); + } else if result.total_warnings > 0 { + tracing::warn!(target: "compiler_compatibility", "{}", summary); + } else { + tracing::info!(target: "compiler_compatibility", "{}", summary); + } + + // Classify: root cause vs cascaded + let deps = + resolved.dependencies.get(package_name).cloned().unwrap_or_default(); + let failed_deps: Vec = deps + .iter() + .filter(|d| outcomes.get(*d).is_some_and(|o| o.total_errors > 0)) + .cloned() + .collect(); + + let failed = result.total_errors > 0; + let root_cause = failed && failed_deps.is_empty(); + let cascaded_from = if failed { failed_deps } else { vec![] }; + + // Transitive root-cause attribution + let cascaded_from_root_causes = if !cascaded_from.is_empty() { + collect_root_causes(&cascaded_from, &outcomes, resolved) + } else { + vec![] + }; + + outcomes.insert( + package_name.clone(), + PackageOutcome { + version, + total_errors: result.total_errors, + total_warnings: result.total_warnings, + topo_layer: layer_index, + root_cause, + cascaded_from, + cascaded_from_root_causes, + }, + ); + } + } + + AllCheckResult { outcomes } +} + +/// Walks transitive dependencies to find all root-cause packages. +fn collect_root_causes( + failed_deps: &[String], + outcomes: &BTreeMap, + resolved: &ResolvedSet, +) -> Vec { + let mut root_causes = Vec::new(); + let mut visited = HashSet::new(); + let mut stack: Vec = failed_deps.to_vec(); + + while let Some(dep) = stack.pop() { + if !visited.insert(dep.clone()) { + continue; + } + if let Some(outcome) = outcomes.get(&dep) { + if outcome.root_cause { + root_causes.push(dep.clone()); + } + // Continue walking through failed transitive deps + if let Some(transitive_deps) = resolved.dependencies.get(&dep) { + for td in transitive_deps { + if outcomes.get(td).is_some_and(|o| o.total_errors > 0) { + stack.push(td.clone()); + } + } + } + } + } + + root_causes.sort(); + root_causes.dedup(); + root_causes +} + /// Builds a module-level dependency graph from indexed imports and returns /// files grouped into topological layers. /// @@ -177,15 +416,15 @@ fn module_topological_layers(engine: &QueryEngine, file_ids: &[FileId]) -> Vec Option { + let prefix = format!("{}-", package_name); let entries = std::fs::read_dir(packages_dir).ok()?; for entry in entries.filter_map(Result::ok) { let name = entry.file_name(); let name_str = name.to_string_lossy(); - if name_str.starts_with(package_name) - && name_str[package_name.len()..].starts_with('-') - && entry.file_type().ok()?.is_dir() - { - return entry.path().canonicalize().ok(); + if let Some(suffix) = name_str.strip_prefix(&prefix) { + if suffix.parse::().is_ok() && entry.file_type().ok()?.is_dir() { + return entry.path().canonicalize().ok(); + } } } None diff --git a/compiler-compatibility/command/src/error.rs b/compiler-compatibility/command/src/error.rs index 148a3168..b4bb50c2 100644 --- a/compiler-compatibility/command/src/error.rs +++ b/compiler-compatibility/command/src/error.rs @@ -20,9 +20,6 @@ pub enum CompatError { #[error("package set missing package: {0}")] MissingFromPackageSet(String), - #[error("version mismatch for {name}: package-set {set_version} not in range {range}")] - VersionMismatch { name: String, set_version: String, range: String }, - #[error("hash mismatch for {name}@{version}")] HashMismatch { name: String, version: String }, diff --git a/compiler-compatibility/command/src/main.rs b/compiler-compatibility/command/src/main.rs index 0f464673..d3921c05 100644 --- a/compiler-compatibility/command/src/main.rs +++ b/compiler-compatibility/command/src/main.rs @@ -2,6 +2,7 @@ mod compat; mod error; mod layout; mod loader; +mod report; mod repositories; mod resolver; mod storage; @@ -9,12 +10,15 @@ mod trace; mod types; mod unpacker; +use std::collections::BTreeMap; use std::path::PathBuf; use clap::Parser; use registry::{FsRegistry, RegistryReader}; use tracing::level_filters::LevelFilter; +use crate::report::{Classification, CompatReport, PackageReport, ReportSummary}; + #[derive(Parser, Debug)] #[command(name = "compiler-compatibility")] #[command(about = "Fetch PureScript packages for compatibility testing")] @@ -22,6 +26,9 @@ struct Cli { #[arg(help = "Package names to fetch and unpack")] packages: Vec, + #[arg(long, help = "Check all packages in the package set")] + all: bool, + #[arg(long, help = "Use specific package set version (default: latest)")] package_set: Option, @@ -43,6 +50,12 @@ struct Cli { #[arg(short, long, help = "Verbose output")] verbose: bool, + #[arg(long, help = "Suppress per-file diagnostic output")] + quiet: bool, + + #[arg(long, value_name = "PATH", help = "Write JSON report to path")] + report_json: Option, + #[arg( long, value_name = "LevelFilter", @@ -55,8 +68,15 @@ struct Cli { fn main() -> error::Result<()> { let cli = Cli::parse(); + if cli.all && !cli.packages.is_empty() { + return Err(error::CompatError::Other( + "Cannot specify both --all and package names".to_string(), + )); + } + let stdout_level = if cli.verbose { LevelFilter::DEBUG } else { LevelFilter::INFO }; - let tracing_handle = trace::init_tracing(stdout_level, cli.log_level, cli.trace_output); + let trace_output = cli.trace_output.clone(); + let tracing_handle = trace::init_tracing(stdout_level, cli.log_level, trace_output); let layout = layout::Layout::new(&cli.output); @@ -72,15 +92,71 @@ fn main() -> error::Result<()> { return Ok(()); } - if cli.packages.is_empty() { - println!("No packages specified. Use --help for usage."); + if !cli.all && cli.packages.is_empty() { + println!( + "No packages specified. Use --all or provide package names. Use --help for usage." + ); return Ok(()); } let package_set = reader.read_package_set(cli.package_set.as_deref())?; tracing::info!(target: "compiler_compatibility", version = %package_set.version, "Using package set"); - let resolved = resolver::resolve(&cli.packages, &package_set, &reader)?; + if cli.all { + run_all_mode(&cli, &package_set, &reader, &layout)?; + } else { + run_packages_mode(&cli, &package_set, &reader, &layout, &tracing_handle)?; + } + + Ok(()) +} + +fn run_all_mode( + cli: &Cli, + package_set: ®istry::PackageSet, + reader: &impl RegistryReader, + layout: &layout::Layout, +) -> error::Result<()> { + let resolved = resolver::resolve_all(package_set, reader)?; + tracing::info!(target: "compiler_compatibility", count = resolved.packages.len(), "Resolved all packages"); + + // Fetch and unpack all packages + for (name, version) in &resolved.packages { + let metadata = reader.read_metadata(name)?; + let published = metadata.published.get(version).ok_or_else(|| { + error::CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } + })?; + + let tarball = storage::fetch_tarball(name, version, layout, cli.no_cache)?; + storage::verify_tarball(&tarball, &published.hash, name, version)?; + unpacker::unpack_tarball(&tarball, &layout.packages)?; + + tracing::debug!(target: "compiler_compatibility", name, version, "Unpacked"); + } + + tracing::info!(target: "compiler_compatibility", directory = %layout.packages.display(), "Finished unpacking"); + + let all_result = compat::check_all(&layout.packages, &resolved, cli.quiet); + + if let Some(ref report_path) = cli.report_json { + let report = build_report(&package_set.version, &all_result); + let json = serde_json::to_string_pretty(&report) + .map_err(|e| error::CompatError::Other(format!("Failed to serialize report: {}", e)))?; + std::fs::write(report_path, json)?; + tracing::info!(target: "compiler_compatibility", path = %report_path.display(), "Report written"); + } + + Ok(()) +} + +fn run_packages_mode( + cli: &Cli, + package_set: ®istry::PackageSet, + reader: &impl RegistryReader, + layout: &layout::Layout, + tracing_handle: &trace::TracingHandle, +) -> error::Result<()> { + let resolved = resolver::resolve(&cli.packages, package_set, reader)?; tracing::info!(target: "compiler_compatibility", count = resolved.packages.len(), "Resolved packages"); for (name, version) in &resolved.packages { @@ -89,7 +165,7 @@ fn main() -> error::Result<()> { error::CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } })?; - let tarball = storage::fetch_tarball(name, version, &layout, cli.no_cache)?; + let tarball = storage::fetch_tarball(name, version, layout, cli.no_cache)?; storage::verify_tarball(&tarball, &published.hash, name, version)?; unpacker::unpack_tarball(&tarball, &layout.packages)?; @@ -111,9 +187,11 @@ fn main() -> error::Result<()> { drop(guard); - for file_result in &result.files { - if !file_result.output.is_empty() { - print!("{}", file_result.output); + if !cli.quiet { + for file_result in &result.files { + if !file_result.output.is_empty() { + print!("{}", file_result.output); + } } } @@ -135,3 +213,62 @@ fn main() -> error::Result<()> { Ok(()) } + +fn build_report(package_set_version: &str, all_result: &compat::AllCheckResult) -> CompatReport { + let mut packages = BTreeMap::new(); + let mut ok = 0; + let mut warnings_only = 0; + let mut failed = 0; + let mut failed_root_cause = 0; + let mut failed_cascaded = 0; + let mut root_causes = Vec::new(); + + for (name, outcome) in &all_result.outcomes { + if outcome.total_errors > 0 { + failed += 1; + if outcome.root_cause { + failed_root_cause += 1; + root_causes.push(name.clone()); + } else { + failed_cascaded += 1; + } + } else if outcome.total_warnings > 0 { + warnings_only += 1; + } else { + ok += 1; + } + + packages.insert( + name.clone(), + PackageReport { + version: outcome.version.clone(), + topo_layer: outcome.topo_layer, + errors: outcome.total_errors, + warnings: outcome.total_warnings, + classification: Classification { + root_cause: outcome.root_cause, + cascaded_from: outcome.cascaded_from.clone(), + cascaded_from_root_causes: outcome.cascaded_from_root_causes.clone(), + }, + }, + ); + } + + let total = all_result.outcomes.len(); + + CompatReport { + timestamp: chrono::Utc::now().to_rfc3339(), + git_sha: std::env::var("GITHUB_SHA").unwrap_or_else(|_| "unknown".to_string()), + package_set: package_set_version.to_string(), + summary: ReportSummary { + total, + ok, + warnings_only, + failed, + failed_root_cause, + failed_cascaded, + }, + root_causes, + packages, + } +} diff --git a/compiler-compatibility/command/src/report.rs b/compiler-compatibility/command/src/report.rs new file mode 100644 index 00000000..665507e2 --- /dev/null +++ b/compiler-compatibility/command/src/report.rs @@ -0,0 +1,39 @@ +use std::collections::BTreeMap; + +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct CompatReport { + pub timestamp: String, + pub git_sha: String, + pub package_set: String, + pub summary: ReportSummary, + pub root_causes: Vec, + pub packages: BTreeMap, +} + +#[derive(Debug, Serialize)] +pub struct ReportSummary { + pub total: usize, + pub ok: usize, + pub warnings_only: usize, + pub failed: usize, + pub failed_root_cause: usize, + pub failed_cascaded: usize, +} + +#[derive(Debug, Serialize)] +pub struct PackageReport { + pub version: String, + pub topo_layer: usize, + pub errors: usize, + pub warnings: usize, + pub classification: Classification, +} + +#[derive(Debug, Serialize)] +pub struct Classification { + pub root_cause: bool, + pub cascaded_from: Vec, + pub cascaded_from_root_causes: Vec, +} diff --git a/compiler-compatibility/command/src/resolver.rs b/compiler-compatibility/command/src/resolver.rs index 94486920..eee700c9 100644 --- a/compiler-compatibility/command/src/resolver.rs +++ b/compiler-compatibility/command/src/resolver.rs @@ -36,6 +36,39 @@ pub fn resolve( Ok(ResolvedSet { packages: resolved, dependencies }) } +/// Resolves all packages in the package set. +/// +/// Unlike `resolve()`, this does not recurse—it iterates all pinned packages +/// and reads their manifests to extract dependency edges. +pub fn resolve_all( + package_set: &PackageSet, + registry: &impl RegistryReader, +) -> Result { + let mut packages = BTreeMap::new(); + let mut dependencies = BTreeMap::new(); + + for (name, version) in &package_set.packages.packages { + packages.insert(name.clone(), version.clone()); + + let manifests = registry.read_manifest_versions(name)?; + let parsed_version: Version = version.parse()?; + let manifest = manifests.iter().find(|m| m.version == parsed_version).ok_or_else(|| { + CompatError::ManifestNotFound { name: name.clone(), version: version.clone() } + })?; + + let dep_names: Vec = manifest + .dependencies + .keys() + .filter(|d| package_set.packages.packages.contains_key(*d)) + .cloned() + .collect(); + + dependencies.insert(name.clone(), dep_names); + } + + Ok(ResolvedSet { packages, dependencies }) +} + fn resolve_recursive( name: &str, version: &str, @@ -59,25 +92,17 @@ fn resolve_recursive( let mut dependency_names = Vec::new(); - for (dep_name, range) in &manifest.dependencies { - let dep_version = package_set + for dependency in manifest.dependencies.keys() { + let version = package_set .packages .packages - .get(dep_name) - .ok_or_else(|| CompatError::MissingFromPackageSet(dep_name.clone()))?; - - if !version_satisfies_range(dep_version, range)? { - return Err(CompatError::VersionMismatch { - name: dep_name.clone(), - set_version: dep_version.clone(), - range: range.clone(), - }); - } + .get(dependency) + .ok_or_else(|| CompatError::MissingFromPackageSet(dependency.clone()))?; - dependency_names.push(dep_name.clone()); + dependency_names.push(dependency.clone()); resolve_recursive( - dep_name, - dep_version, + dependency, + version, package_set, registry, resolved, @@ -174,24 +199,3 @@ pub fn topological_layers( layers } - -fn version_satisfies_range(version: &str, range: &str) -> Result { - let parsed_version: Version = version.parse()?; - let parts: Vec<&str> = range.split_whitespace().collect(); - - if parts.len() != 2 { - return Err(CompatError::Other(format!("invalid version range format: {}", range))); - } - - let lower = parts[0] - .strip_prefix(">=") - .ok_or_else(|| CompatError::Other(format!("expected >= prefix in range: {}", range)))?; - let upper = parts[1] - .strip_prefix('<') - .ok_or_else(|| CompatError::Other(format!("expected < prefix in range: {}", range)))?; - - let lower_version: Version = lower.parse()?; - let upper_version: Version = upper.parse()?; - - Ok(parsed_version >= lower_version && parsed_version < upper_version) -} diff --git a/compiler-compatibility/registry/src/types.rs b/compiler-compatibility/registry/src/types.rs index bcc4656f..48b38241 100644 --- a/compiler-compatibility/registry/src/types.rs +++ b/compiler-compatibility/registry/src/types.rs @@ -53,7 +53,8 @@ pub enum Location { #[derive(Debug, Clone, Deserialize)] pub struct Metadata { pub location: Location, - pub owners: Option>, + #[serde(default)] + pub owners: serde_json::Value, pub published: HashMap, #[serde(default)] pub unpublished: HashMap, From 634c079ba2175d73198aa99cb4dadd36312415e5 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 18:03:25 +0800 Subject: [PATCH 169/386] Add support for open rows in Prim.Row constraints --- .../checking/src/algorithm/constraint.rs | 53 ++++++++- .../constraint/compiler_solved/prim_row.rs | 104 +++++++++++++++--- .../checking/333_row_open_union/Main.purs | 22 ++++ .../checking/333_row_open_union/Main.snap | 44 ++++++++ .../checking/334_row_open_cons/Main.purs | 17 +++ .../checking/334_row_open_cons/Main.snap | 37 +++++++ .../checking/335_row_open_lacks/Main.purs | 17 +++ .../checking/335_row_open_lacks/Main.snap | 35 ++++++ .../checking/336_row_open_record/Main.purs | 21 ++++ .../checking/336_row_open_record/Main.snap | 25 +++++ tests-integration/tests/checking/generated.rs | 8 ++ 11 files changed, 362 insertions(+), 21 deletions(-) create mode 100644 tests-integration/fixtures/checking/333_row_open_union/Main.purs create mode 100644 tests-integration/fixtures/checking/333_row_open_union/Main.snap create mode 100644 tests-integration/fixtures/checking/334_row_open_cons/Main.purs create mode 100644 tests-integration/fixtures/checking/334_row_open_cons/Main.snap create mode 100644 tests-integration/fixtures/checking/335_row_open_lacks/Main.purs create mode 100644 tests-integration/fixtures/checking/335_row_open_lacks/Main.snap create mode 100644 tests-integration/fixtures/checking/336_row_open_record/Main.purs create mode 100644 tests-integration/fixtures/checking/336_row_open_record/Main.snap diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index e22e3928..0d7ad934 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -686,7 +686,9 @@ where result = result.and_also(|| { match_type(state, context, bindings, equalities, wanted.id, given.id) }); - if matches!(result, MatchType::Apart) { + // Given an open wanted row, additional fields from the + // given row can be absorbed into the wanted row's tail. + if matches!(result, MatchType::Apart) && wanted_row.tail.is_none() { return MatchType::Apart; } } @@ -826,6 +828,51 @@ where match_given_type(state, context, wanted, given) } + (Type::Row(wanted_row), Type::Row(given_row)) => { + if wanted_row.fields.len() != given_row.fields.len() { + return MatchType::Apart; + } + + let wanted_fields = Arc::clone(&wanted_row.fields); + let given_fields = Arc::clone(&given_row.fields); + + let wanted_tail = wanted_row.tail; + let given_tail = given_row.tail; + + let mut result = MatchType::Match; + for (wanted_field, given_field) in iter::zip(wanted_fields.iter(), given_fields.iter()) + { + if wanted_field.label != given_field.label { + return MatchType::Apart; + } + result = result + .and_also(|| match_given_type(state, context, wanted_field.id, given_field.id)); + } + + match (wanted_tail, given_tail) { + (Some(wanted_tail), Some(given_tail)) => { + result.and_also(|| match_given_type(state, context, wanted_tail, given_tail)) + } + (Some(wanted_tail), None) => { + let wanted_tail = state.normalize_type(wanted_tail); + if matches!(state.storage[wanted_tail], Type::Unification(_)) { + result.and_also(|| MatchType::Stuck) + } else { + MatchType::Apart + } + } + (None, Some(given_tail)) => { + let given_tail = state.normalize_type(given_tail); + if matches!(state.storage[given_tail], Type::Unification(_)) { + result.and_also(|| MatchType::Stuck) + } else { + MatchType::Apart + } + } + (None, None) => result, + } + } + ( &Type::KindApplication(w_function, w_argument), &Type::KindApplication(g_function, g_argument), @@ -1162,9 +1209,9 @@ where cons => prim_symbol_cons(state, arguments), }, context.prim_row => { - union => prim_row_union(state, arguments), + union => prim_row_union(state, context, arguments), cons => prim_row_cons(state, arguments), - lacks => prim_row_lacks(state, arguments), + lacks => prim_row_lacks(state, context, arguments), nub => prim_row_nub(state, arguments), }, context.prim_row_list => { diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs index a281aa92..3ab8ff8e 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs @@ -3,8 +3,9 @@ use std::iter; use rustc_hash::FxHashSet; +use crate::ExternalQueries; use crate::algorithm::constraint::{self, MatchInstance}; -use crate::algorithm::state::CheckState; +use crate::algorithm::state::{CheckContext, CheckState}; use crate::core::{RowField, RowType}; use crate::{Type, TypeId}; @@ -65,7 +66,14 @@ fn subtract_row_fields( Some((result, equalities)) } -pub fn prim_row_union(state: &mut CheckState, arguments: &[TypeId]) -> Option { +pub fn prim_row_union( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> Option +where + Q: ExternalQueries, +{ let &[left, right, union] = arguments else { return None; }; @@ -74,25 +82,61 @@ pub fn prim_row_union(state: &mut CheckState, arguments: &[TypeId]) -> Option { - let left_fields = left_row.fields.iter(); - let right_fields = right_row.fields.iter(); + if let Some(rest) = left_row.tail { + if left_row.fields.is_empty() { + return Some(MatchInstance::Stuck); + } + + let fresh_tail = state.fresh_unification_kinded(context.prim.row_type); + + let result = state.storage.intern(Type::Row(RowType::from_unsorted( + left_row.fields.to_vec(), + Some(fresh_tail), + ))); + + let prim_row = &context.prim_row; + + let constraint = + state.storage.intern(Type::Constructor(prim_row.file_id, prim_row.union)); + + let constraint = state.storage.intern(Type::Application(constraint, rest)); + let constraint = state.storage.intern(Type::Application(constraint, right)); + let constraint = state.storage.intern(Type::Application(constraint, fresh_tail)); + + return Some(MatchInstance::Match { + constraints: vec![constraint], + equalities: vec![(union, result)], + }); + } - let union_fields = iter::chain(left_fields, right_fields).cloned().collect(); - let result = state.storage.intern(Type::Row(RowType::closed(union_fields))); + let union_fields = { + let left = left_row.fields.iter(); + let right = right_row.fields.iter(); + iter::chain(left, right).cloned().collect() + }; + + let result = state + .storage + .intern(Type::Row(RowType::from_unsorted(union_fields, right_row.tail))); Some(MatchInstance::Match { constraints: vec![], equalities: vec![(union, result)] }) } (_, Some(right_row), Some(union_row)) => { + if right_row.tail.is_some() { + return Some(MatchInstance::Stuck); + } if let Some((remaining, mut equalities)) = subtract_row_fields(state, &union_row.fields, &right_row.fields) { - let result = state.storage.intern(Type::Row(RowType::closed(remaining))); + let result = state + .storage + .intern(Type::Row(RowType::from_unsorted(remaining, union_row.tail))); equalities.push((left, result)); Some(MatchInstance::Match { constraints: vec![], equalities }) } else { @@ -100,10 +144,15 @@ pub fn prim_row_union(state: &mut CheckState, arguments: &[TypeId]) -> Option { + if left_row.tail.is_some() { + return Some(MatchInstance::Stuck); + } if let Some((remaining, mut equalities)) = subtract_row_fields(state, &union_row.fields, &left_row.fields) { - let result = state.storage.intern(Type::Row(RowType::closed(remaining))); + let result = state + .storage + .intern(Type::Row(RowType::from_unsorted(remaining, union_row.tail))); equalities.push((right, result)); Some(MatchInstance::Match { constraints: vec![], equalities }) } else { @@ -125,15 +174,15 @@ pub fn prim_row_cons(state: &mut CheckState, arguments: &[TypeId]) -> Option { let mut fields = vec![RowField { label: label_value, id: a }]; fields.extend(tail_row.fields.iter().cloned()); - let result_row = RowType::from_unsorted(fields, None); + let result_row = RowType::from_unsorted(fields, tail_row.tail); let result = state.storage.intern(Type::Row(result_row)); Some(MatchInstance::Match { constraints: vec![], equalities: vec![(row, result)] }) @@ -152,7 +201,9 @@ pub fn prim_row_cons(state: &mut CheckState, arguments: &[TypeId]) -> Option Option Option { +pub fn prim_row_lacks( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> Option +where + Q: ExternalQueries, +{ let &[label, row] = arguments else { return None; }; @@ -185,8 +243,18 @@ pub fn prim_row_lacks(state: &mut CheckState, arguments: &[TypeId]) -> Option Type +data Proxy a = Proxy + +foreign import unsafeCoerce :: forall a b. a -> b + +openLeft :: forall r u. Row.Union (a :: Int | r) (b :: String) u => Proxy u +openLeft = Proxy + +openRight :: forall r u. Row.Union (a :: Int) (b :: String | r) u => Proxy u +openRight = Proxy + +backwardLeft :: forall l r. Row.Union l (b :: String) (a :: Int, b :: String | r) => Proxy l +backwardLeft = Proxy + +backwardRight :: forall r u. Row.Union (a :: Int) r (a :: Int, b :: String | u) => Proxy r +backwardRight = Proxy + +forceSolve = { openLeft, openRight, backwardLeft, backwardRight } diff --git a/tests-integration/fixtures/checking/333_row_open_union/Main.snap b/tests-integration/fixtures/checking/333_row_open_union/Main.snap new file mode 100644 index 00000000..ac167160 --- /dev/null +++ b/tests-integration/fixtures/checking/333_row_open_union/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +openLeft :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int | (r :: Row Type) ) ( b :: String ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +openRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) ( b :: String | (r :: Row Type) ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +backwardLeft :: + forall (l :: Row Type) (r :: Row Type). + Union @Type (l :: Row Type) ( b :: String ) ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (l :: Row Type) +backwardRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) (r :: Row Type) ( a :: Int, b :: String | (u :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) +forceSolve :: + forall (t38 :: Row Type) (t40 :: Row Type) (t43 :: Row Type) (t45 :: Row Type) (t46 :: Row Type). + Union (t38 :: Row Type) ( b :: String ) (t46 :: Row Type) => + { backwardLeft :: Proxy @(Row Type) ( a :: Int | (t43 :: Row Type) ) + , backwardRight :: Proxy @(Row Type) ( b :: String | (t45 :: Row Type) ) + , openLeft :: Proxy @(Row Type) ( a :: Int | (t46 :: Row Type) ) + , openRight :: Proxy @(Row Type) ( a :: Int, b :: String | (t40 :: Row Type) ) + } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Data +Proxy + Quantified = :0 + Kind = :1 + + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking/334_row_open_cons/Main.purs b/tests-integration/fixtures/checking/334_row_open_cons/Main.purs new file mode 100644 index 00000000..6a881ce7 --- /dev/null +++ b/tests-integration/fixtures/checking/334_row_open_cons/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Prim.Row as Row + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +consOpen :: forall r row. Row.Cons "x" Int (a :: String | r) row => Proxy row +consOpen = Proxy + +decomposeOpen :: forall t tail r. Row.Cons "x" t tail (x :: Int, a :: String | r) => Proxy t +decomposeOpen = Proxy + +extractTail :: forall tail r. Row.Cons "x" Int tail (x :: Int, a :: String | r) => Proxy tail +extractTail = Proxy + +forceSolve = { consOpen, decomposeOpen, extractTail } diff --git a/tests-integration/fixtures/checking/334_row_open_cons/Main.snap b/tests-integration/fixtures/checking/334_row_open_cons/Main.snap new file mode 100644 index 00000000..10ba85eb --- /dev/null +++ b/tests-integration/fixtures/checking/334_row_open_cons/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +consOpen :: + forall (r :: Row Type) (row :: Row Type). + Cons @Type "x" Int ( a :: String | (r :: Row Type) ) (row :: Row Type) => + Proxy @(Row Type) (row :: Row Type) +decomposeOpen :: + forall (t :: Type) (tail :: Row Type) (r :: Row Type). + Cons @Type "x" (t :: Type) (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @Type (t :: Type) +extractTail :: + forall (tail :: Row Type) (r :: Row Type). + Cons @Type "x" Int (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @(Row Type) (tail :: Row Type) +forceSolve :: + forall (t26 :: Row Type) (t32 :: Row Type). + { consOpen :: Proxy @(Row Type) ( a :: String, x :: Int | (t26 :: Row Type) ) + , decomposeOpen :: Proxy @Type Int + , extractTail :: Proxy @(Row Type) ( a :: String | (t32 :: Row Type) ) + } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Data +Proxy + Quantified = :0 + Kind = :1 + + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking/335_row_open_lacks/Main.purs b/tests-integration/fixtures/checking/335_row_open_lacks/Main.purs new file mode 100644 index 00000000..84c7849c --- /dev/null +++ b/tests-integration/fixtures/checking/335_row_open_lacks/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Prim.Row as Row + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +lacksOpen :: forall r. Row.Lacks "missing" (a :: Int, b :: String | r) => Proxy r -> Int +lacksOpen _ = 0 + +lacksPresent :: forall r. Row.Lacks "a" (a :: Int | r) => Proxy r -> Int +lacksPresent _ = 0 + +empty :: Proxy () +empty = Proxy + +forceSolve = { lacksOpen: lacksOpen empty, lacksPresent: lacksPresent empty } diff --git a/tests-integration/fixtures/checking/335_row_open_lacks/Main.snap b/tests-integration/fixtures/checking/335_row_open_lacks/Main.snap new file mode 100644 index 00000000..6e55786a --- /dev/null +++ b/tests-integration/fixtures/checking/335_row_open_lacks/Main.snap @@ -0,0 +1,35 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +lacksOpen :: + forall (r :: Row Type). + Lacks @Type "missing" ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) -> Int +lacksPresent :: + forall (r :: Row Type). + Lacks @Type "a" ( a :: Int | (r :: Row Type) ) => Proxy @(Row Type) (r :: Row Type) -> Int +empty :: forall (t13 :: Type). Proxy @(Row (t13 :: Type)) () +forceSolve :: { lacksOpen :: Int, lacksPresent :: Int } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Data +Proxy + Quantified = :0 + Kind = :1 + + +Roles +Proxy = [Phantom] + +Diagnostics +error[NoInstanceFound]: No instance found for: Lacks @Type "a" ( a :: Int | () ) + --> 17:1..17:78 + | +17 | forceSolve = { lacksOpen: lacksOpen empty, lacksPresent: lacksPresent empty } + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests-integration/fixtures/checking/336_row_open_record/Main.purs b/tests-integration/fixtures/checking/336_row_open_record/Main.purs new file mode 100644 index 00000000..9ab0137a --- /dev/null +++ b/tests-integration/fixtures/checking/336_row_open_record/Main.purs @@ -0,0 +1,21 @@ +module Main where + +import Prim.Row as Row + +foreign import unsafeCoerce :: forall a b. a -> b + +union :: forall r1 r2 r3. Row.Union r1 r2 r3 => Record r1 -> Record r2 -> Record r3 +union _ _ = unsafeCoerce {} + +addField :: forall r. { a :: Int | r } -> { a :: Int, b :: String | r } +addField x = union x { b: "hi" } + +test = addField { a: 1, c: true } + +insertX :: forall r. Row.Lacks "x" r => Record r -> Record (x :: Int | r) +insertX _ = unsafeCoerce {} + +insertOpen :: forall r. Row.Lacks "x" r => { a :: Int | r } -> { x :: Int, a :: Int | r } +insertOpen x = insertX x + +test2 = insertOpen { a: 1, b: "hi" } diff --git a/tests-integration/fixtures/checking/336_row_open_record/Main.snap b/tests-integration/fixtures/checking/336_row_open_record/Main.snap new file mode 100644 index 00000000..41376d9d --- /dev/null +++ b/tests-integration/fixtures/checking/336_row_open_record/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +union :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r3 :: Row Type) } +addField :: + forall (r :: Row Type). + { a :: Int | (r :: Row Type) } -> { a :: Int, b :: String | (r :: Row Type) } +test :: { a :: Int, b :: String | ( c :: Boolean ) } +insertX :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => {| (r :: Row Type) } -> { x :: Int | (r :: Row Type) } +insertOpen :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => + { a :: Int | (r :: Row Type) } -> { a :: Int, x :: Int | (r :: Row Type) } +test2 :: { a :: Int, x :: Int | ( b :: String ) } + +Types diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index b9a170c6..265b3bbb 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -677,3 +677,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_331_record_progressive_labels_main() { run_test("331_record_progressive_labels", "Main"); } #[rustfmt::skip] #[test] fn test_332_record_equation_labels_main() { run_test("332_record_equation_labels", "Main"); } + +#[rustfmt::skip] #[test] fn test_333_row_open_union_main() { run_test("333_row_open_union", "Main"); } + +#[rustfmt::skip] #[test] fn test_334_row_open_cons_main() { run_test("334_row_open_cons", "Main"); } + +#[rustfmt::skip] #[test] fn test_335_row_open_lacks_main() { run_test("335_row_open_lacks", "Main"); } + +#[rustfmt::skip] #[test] fn test_336_row_open_record_main() { run_test("336_row_open_record", "Main"); } From ccf96f780471724ab053e59bfc3602a85850b9de Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 20:37:00 +0800 Subject: [PATCH 170/386] Combine signature and definition checking This fixes a bug where foreign data declarations fail to check when they depend on data declarations. Previously, the data declaration would have been checked later than the foreign data declaration, causing an error. --- compiler-core/checking/src/algorithm.rs | 55 ++++--------------- .../checking/006_type_synonym/Main.snap | 7 ++- .../fixtures/checking/337_void_data/Main.purs | 6 ++ .../fixtures/checking/337_void_data/Main.snap | 22 ++++++++ tests-integration/tests/checking/generated.rs | 2 + 5 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 tests-integration/fixtures/checking/337_void_data/Main.purs create mode 100644 tests-integration/fixtures/checking/337_void_data/Main.snap diff --git a/compiler-core/checking/src/algorithm.rs b/compiler-core/checking/src/algorithm.rs index 82b674c9..ff25ed9d 100644 --- a/compiler-core/checking/src/algorithm.rs +++ b/compiler-core/checking/src/algorithm.rs @@ -81,8 +81,7 @@ pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryRes let mut state = state::CheckState::new(file_id); let context = state::CheckContext::new(queries, &mut state, file_id)?; - check_type_signatures(&mut state, &context)?; - check_type_definitions(&mut state, &context)?; + check_types(&mut state, &context)?; type_item::commit_pending_types(&mut state, &context); check_term_signatures(&mut state, &context)?; @@ -97,50 +96,13 @@ pub fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryRes Ok(state.checked) } -/// See [`type_item::check_type_signature`] +/// Checks all type declarations in topological order. /// -/// Kind signatures are acyclic, and can be checked separately from the -/// type definitions. Checking these early adds better information for -/// inference, especially for mutually recursive type declarations. +/// Within [`Scc::Mutual`], kind signatures are checked first so that items with +/// signatures provide better information when inferring items with no signatures. /// -/// Consider the following example: -/// -/// ```purescript -/// data F a = MkF (G a) -/// -/// data G :: Int -> Type -/// data G a = MkG (F a) -/// ``` -/// -/// By checking the kind signature of `G` first, we can avoid allocating -/// a unification variable for `G` when checking the mutually recursive -/// declarations of `{F, G}` -fn check_type_signatures( - state: &mut state::CheckState, - context: &state::CheckContext, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for scc in &context.grouped.type_scc { - let items = match scc { - Scc::Base(id) | Scc::Recursive(id) => slice::from_ref(id), - Scc::Mutual(items) => items, - }; - for id in items { - type_item::check_type_signature(state, context, *id)?; - } - } - Ok(()) -} - -/// See [`type_item::check_type_item`] -/// -/// This function calls [`state::CheckState::with_type_group`] to insert -/// placeholder unification variables for recursive binding groups. After -/// checking a binding group, it calls [`type_item::commit_type_item`] to -/// generalise the types and add them to [`state::CheckState::checked`]. -fn check_type_definitions( +/// See [`type_item::check_type_signature`] and [`type_item::check_type_item`]. +fn check_types( state: &mut state::CheckState, context: &state::CheckContext, ) -> QueryResult<()> @@ -150,11 +112,13 @@ where for scc in &context.grouped.type_scc { match scc { Scc::Base(id) => { + type_item::check_type_signature(state, context, *id)?; if let Some(item) = type_item::check_type_item(state, context, *id)? { type_item::commit_type_item(state, context, *id, item)?; } } Scc::Recursive(id) => { + type_item::check_type_signature(state, context, *id)?; state.with_type_group(context, [*id], |state| { if let Some(item) = type_item::check_type_item(state, context, *id)? { type_item::commit_type_item(state, context, *id, item)?; @@ -163,6 +127,9 @@ where })?; } Scc::Mutual(mutual) => { + for id in mutual { + type_item::check_type_signature(state, context, *id)?; + } state.with_type_group(context, mutual, |state| { let mut items = vec![]; for &id in mutual { diff --git a/tests-integration/fixtures/checking/006_type_synonym/Main.snap b/tests-integration/fixtures/checking/006_type_synonym/Main.snap index 47dc5a18..a6ec3f83 100644 --- a/tests-integration/fixtures/checking/006_type_synonym/Main.snap +++ b/tests-integration/fixtures/checking/006_type_synonym/Main.snap @@ -1,5 +1,6 @@ --- source: tests-integration/tests/checking/generated.rs +assertion_line: 28 expression: report --- Terms @@ -9,7 +10,7 @@ Tuple :: Type -> Type -> Type AliasType :: Type AliasTypeType :: Type -> Type InferApply :: - forall (t4 :: Type) (t9 :: Type). ((t9 :: Type) -> (t4 :: Type)) -> (t9 :: Type) -> (t4 :: Type) + forall (t2 :: Type) (t7 :: Type). ((t7 :: Type) -> (t2 :: Type)) -> (t7 :: Type) -> (t2 :: Type) InferTuple :: Type -> Type -> Type CheckApply :: forall (x :: Type) (y :: Type). ((x :: Type) -> (y :: Type)) -> (x :: Type) -> (y :: Type) @@ -27,8 +28,8 @@ AliasTypeType = Array Kind = :0 Type = :0 -InferApply = forall (t4 :: Type) (t9 :: Type) (f :: (t9 :: Type) -> (t4 :: Type)) (a :: (t9 :: Type)). - (f :: (t9 :: Type) -> (t4 :: Type)) (a :: (t9 :: Type)) +InferApply = forall (t2 :: Type) (t7 :: Type) (f :: (t7 :: Type) -> (t2 :: Type)) (a :: (t7 :: Type)). + (f :: (t7 :: Type) -> (t2 :: Type)) (a :: (t7 :: Type)) Quantified = :2 Kind = :0 Type = :2 diff --git a/tests-integration/fixtures/checking/337_void_data/Main.purs b/tests-integration/fixtures/checking/337_void_data/Main.purs new file mode 100644 index 00000000..bda30cdb --- /dev/null +++ b/tests-integration/fixtures/checking/337_void_data/Main.purs @@ -0,0 +1,6 @@ +module Main where + +data SList + +foreign import data SCons :: Symbol -> SList -> SList +foreign import data SNil :: SList diff --git a/tests-integration/fixtures/checking/337_void_data/Main.snap b/tests-integration/fixtures/checking/337_void_data/Main.snap new file mode 100644 index 00000000..9e7f9d15 --- /dev/null +++ b/tests-integration/fixtures/checking/337_void_data/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +SList :: Type +SCons :: Symbol -> SList -> SList +SNil :: SList + +Data +SList + Quantified = :0 + Kind = :0 + + +Roles +SList = [] +SCons = [Nominal, Nominal] +SNil = [] diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 265b3bbb..6b691d14 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -685,3 +685,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_335_row_open_lacks_main() { run_test("335_row_open_lacks", "Main"); } #[rustfmt::skip] #[test] fn test_336_row_open_record_main() { run_test("336_row_open_record", "Main"); } + +#[rustfmt::skip] #[test] fn test_337_void_data_main() { run_test("337_void_data", "Main"); } From 1e367f4c5b52034645f66c72859939b2e42d0645 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 20:41:28 +0800 Subject: [PATCH 171/386] Format files --- compiler-compatibility/command/src/compat.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 91c80711..06975d09 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -271,10 +271,8 @@ pub fn check_all(packages_dir: &Path, resolved: &ResolvedSet, quiet: bool) -> Al ) .entered(); - let pkg_files = - package_files.get(package_name).map(|v| v.as_slice()).unwrap_or(&[]); - let version = - resolved.packages.get(package_name).cloned().unwrap_or_default(); + let pkg_files = package_files.get(package_name).map(|v| v.as_slice()).unwrap_or(&[]); + let version = resolved.packages.get(package_name).cloned().unwrap_or_default(); let result = if pkg_files.is_empty() { // Synthetic error for missing package directory @@ -282,10 +280,7 @@ pub fn check_all(packages_dir: &Path, resolved: &ResolvedSet, quiet: bool) -> Al files: vec![FileResult { error_count: 1, warning_count: 0, - output: format!( - "{}: error: package directory not found\n", - package_name - ), + output: format!("{}: error: package directory not found\n", package_name), }], total_errors: 1, total_warnings: 0, @@ -317,8 +312,7 @@ pub fn check_all(packages_dir: &Path, resolved: &ResolvedSet, quiet: bool) -> Al } // Classify: root cause vs cascaded - let deps = - resolved.dependencies.get(package_name).cloned().unwrap_or_default(); + let deps = resolved.dependencies.get(package_name).cloned().unwrap_or_default(); let failed_deps: Vec = deps .iter() .filter(|d| outcomes.get(*d).is_some_and(|o| o.total_errors > 0)) From cfb0f4895f07d3c3c54794be93848db109a18e68 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 22:44:49 +0800 Subject: [PATCH 172/386] Fix issue with overlap in exported modules The bug manifests when two exported modules contain overlapping names. The previous representation assumed that we can merge names across qualified imports, generating errors for duplicates. --- compiler-core/diagnostics/src/convert.rs | 4 +- compiler-core/resolving/src/algorithm.rs | 107 +++------------- compiler-core/resolving/src/error.rs | 9 -- compiler-core/resolving/src/lib.rs | 31 +++-- .../analyzer/src/completion/sources.rs | 120 ++++++++++-------- compiler-lsp/analyzer/src/references.rs | 4 +- .../IsInt.purs | 4 + .../IsString.purs | 7 + .../338_module_export_alias_overlap/Lib.purs | 4 + .../338_module_export_alias_overlap/Main.purs | 5 + .../338_module_export_alias_overlap/Main.snap | 9 ++ .../ImportQualifiedExplicit.snap | 6 +- .../ImportQualifiedHiding.snap | 6 +- .../ImportQualifiedImplicit.snap | 10 +- .../ImportUnqualifiedExplicit.snap | 6 +- .../ImportUnqualifiedHiding.snap | 6 +- .../ImportUnqualifiedImplicit.snap | 10 +- .../DuplicateQualifiedImport.snap | 10 +- tests-integration/src/generated/basic.rs | 44 ++++--- tests-integration/tests/checking/generated.rs | 2 + 20 files changed, 191 insertions(+), 213 deletions(-) create mode 100644 tests-integration/fixtures/checking/338_module_export_alias_overlap/IsInt.purs create mode 100644 tests-integration/fixtures/checking/338_module_export_alias_overlap/IsString.purs create mode 100644 tests-integration/fixtures/checking/338_module_export_alias_overlap/Lib.purs create mode 100644 tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.purs create mode 100644 tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.snap diff --git a/compiler-core/diagnostics/src/convert.rs b/compiler-core/diagnostics/src/convert.rs index 7d9ec68f..c1056f2b 100644 --- a/compiler-core/diagnostics/src/convert.rs +++ b/compiler-core/diagnostics/src/convert.rs @@ -119,9 +119,7 @@ fn convert_recursive_group( impl ToDiagnostics for ResolvingError { fn to_diagnostics(&self, ctx: &DiagnosticsContext<'_>) -> Vec { match self { - ResolvingError::TermImportConflict { .. } - | ResolvingError::TypeImportConflict { .. } - | ResolvingError::TermExportConflict { .. } + ResolvingError::TermExportConflict { .. } | ResolvingError::TypeExportConflict { .. } | ResolvingError::ExistingTerm { .. } | ResolvingError::ExistingType { .. } => { diff --git a/compiler-core/resolving/src/algorithm.rs b/compiler-core/resolving/src/algorithm.rs index 10afb5a4..5aaf5f3f 100644 --- a/compiler-core/resolving/src/algorithm.rs +++ b/compiler-core/resolving/src/algorithm.rs @@ -1,8 +1,8 @@ use building_types::QueryResult; use files::FileId; use indexing::{ - ExportKind, ImplicitItems, ImportId, ImportKind, IndexedModule, IndexingImport, TermItemId, - TermItemKind, TypeItemId, TypeItemKind, + ExportKind, ImplicitItems, ImportKind, IndexedModule, IndexingImport, TermItemId, TermItemKind, + TypeItemId, TypeItemKind, }; use smol_str::SmolStr; @@ -56,13 +56,14 @@ fn resolve_imports( if let Some(alias) = &indexing_import.alias { let alias = SmolStr::clone(alias); - let resolved_import = state.qualified.entry(alias).or_insert(resolved_import); + let imports = state.qualified.entry(alias).or_default(); + imports.push(resolved_import); + let resolved_import = imports.last_mut().unwrap(); resolve_import( queries, &mut state.errors, &mut state.class, resolved_import, - indexing_import_id, indexing_import, import_file_id, )?; @@ -73,7 +74,6 @@ fn resolve_imports( &mut state.errors, &mut state.class, &mut resolved_import, - indexing_import_id, indexing_import, import_file_id, )?; @@ -89,7 +89,6 @@ fn resolve_import( errors: &mut Vec, class_members: &mut ResolvedClassMembers, resolved: &mut ResolvedImport, - indexing_import_id: ImportId, indexing_import: &IndexingImport, import_file_id: FileId, ) -> QueryResult<()> { @@ -106,9 +105,9 @@ fn resolve_import( let classes = import_resolved.exports.iter_classes().map(|(name, file, id)| (name, file, id, kind)); - add_imported_terms(errors, resolved, indexing_import_id, terms); - add_imported_types(errors, resolved, indexing_import_id, types); - add_imported_classes(errors, resolved, indexing_import_id, classes); + add_imported_terms(resolved, terms); + add_imported_types(resolved, types); + add_imported_classes(resolved, classes); // Adjust import kinds for explicit/hidden imports BEFORE copying class members if !matches!(indexing_import.kind, ImportKind::Implicit) { @@ -183,102 +182,36 @@ fn resolve_implicit( } fn add_imported_terms<'a>( - errors: &mut Vec, resolved: &mut ResolvedImport, - indexing_import_id: ImportId, terms: impl Iterator, ) { let (additional, _) = terms.size_hint(); resolved.terms.reserve(additional); - terms.for_each(|(name, file, id, kind)| { - add_imported_term(errors, resolved, indexing_import_id, name, file, id, kind); - }); -} - -fn add_imported_types<'a>( - errors: &mut Vec, - resolved: &mut ResolvedImport, - indexing_import_id: ImportId, - terms: impl Iterator, -) { - let (additional, _) = terms.size_hint(); - resolved.terms.reserve(additional); - terms.for_each(|(name, file, id, kind)| { - add_imported_type(errors, resolved, indexing_import_id, name, file, id, kind); - }); -} - -fn add_imported_classes<'a>( - errors: &mut Vec, - resolved: &mut ResolvedImport, - indexing_import_id: ImportId, - terms: impl Iterator, -) { - let (additional, _) = terms.size_hint(); - resolved.classes.reserve(additional); - terms.for_each(|(name, file, id, kind)| { - add_imported_class(errors, resolved, indexing_import_id, name, file, id, kind); - }); -} - -fn add_imported_term( - errors: &mut Vec, - resolved: &mut ResolvedImport, - indexing_import_id: ImportId, - name: &SmolStr, - file: FileId, - id: TermItemId, - kind: ImportKind, -) { - if let Some((existing_file, existing_term, _)) = resolved.terms.get(name) { - let duplicate = (file, id, indexing_import_id); - let existing = (*existing_file, *existing_term, resolved.id); - if duplicate != existing { - errors.push(ResolvingError::TermImportConflict { existing, duplicate }); - } - } else { + for (name, file, id, kind) in terms { let name = SmolStr::clone(name); resolved.terms.insert(name, (file, id, kind)); } } -fn add_imported_type( - errors: &mut Vec, +fn add_imported_types<'a>( resolved: &mut ResolvedImport, - indexing_import_id: ImportId, - name: &SmolStr, - file: FileId, - id: TypeItemId, - kind: ImportKind, + types: impl Iterator, ) { - if let Some((existing_file, existing_term, _)) = resolved.types.get(name) { - let duplicate = (file, id, indexing_import_id); - let existing = (*existing_file, *existing_term, resolved.id); - if duplicate != existing { - errors.push(ResolvingError::TypeImportConflict { existing, duplicate }); - } - } else { + let (additional, _) = types.size_hint(); + resolved.types.reserve(additional); + for (name, file, id, kind) in types { let name = SmolStr::clone(name); resolved.types.insert(name, (file, id, kind)); } } -fn add_imported_class( - errors: &mut Vec, +fn add_imported_classes<'a>( resolved: &mut ResolvedImport, - indexing_import_id: ImportId, - name: &SmolStr, - file: FileId, - id: TypeItemId, - kind: ImportKind, + classes: impl Iterator, ) { - if let Some((existing_file, existing_term, _)) = resolved.classes.get(name) { - let duplicate = (file, id, indexing_import_id); - let existing = (*existing_file, *existing_term, resolved.id); - if duplicate != existing { - errors.push(ResolvingError::TypeImportConflict { existing, duplicate }); - } - } else { + let (additional, _) = classes.size_hint(); + resolved.classes.reserve(additional); + for (name, file, id, kind) in classes { let name = SmolStr::clone(name); resolved.classes.insert(name, (file, id, kind)); } @@ -458,7 +391,7 @@ fn export_module_imports(state: &mut State, indexed: &IndexedModule) { } let unqualified = state.unqualified.values().flatten(); - let qualified = state.qualified.values(); + let qualified = state.qualified.values().flatten(); let imports = unqualified.chain(qualified); for import in imports { diff --git a/compiler-core/resolving/src/error.rs b/compiler-core/resolving/src/error.rs index ef5f281a..4ee624ba 100644 --- a/compiler-core/resolving/src/error.rs +++ b/compiler-core/resolving/src/error.rs @@ -6,15 +6,6 @@ use crate::ExportSource; /// The kind of errors produced during name resolution. #[derive(Debug, PartialEq, Eq)] pub enum ResolvingError { - TermImportConflict { - existing: (FileId, TermItemId, ImportId), - duplicate: (FileId, TermItemId, ImportId), - }, - TypeImportConflict { - existing: (FileId, TypeItemId, ImportId), - duplicate: (FileId, TypeItemId, ImportId), - }, - TermExportConflict { existing: (FileId, TermItemId, ExportSource), duplicate: (FileId, TermItemId, ExportSource), diff --git a/compiler-core/resolving/src/lib.rs b/compiler-core/resolving/src/lib.rs index fcb37726..ba08ad4e 100644 --- a/compiler-core/resolving/src/lib.rs +++ b/compiler-core/resolving/src/lib.rs @@ -69,12 +69,15 @@ impl ResolvedModule { default: DefaultFn, ) -> Option<(FileId, ItemId)> where - LookupFn: FnOnce(&ResolvedImport) -> Option<(FileId, ItemId, ImportKind)>, + LookupFn: Fn(&ResolvedImport) -> Option<(FileId, ItemId, ImportKind)>, DefaultFn: FnOnce() -> Option<(FileId, ItemId)>, { - if let Some(import) = self.qualified.get(qualifier) { - let (file, id, kind) = lookup(import)?; - if matches!(kind, ImportKind::Hidden) { None } else { Some((file, id)) } + if let Some(imports) = self.qualified.get(qualifier) { + let (file_id, item_id, _) = imports + .iter() + .filter_map(&lookup) + .find(|(_, _, kind)| !matches!(kind, ImportKind::Hidden))?; + Some((file_id, item_id)) } else if qualifier == "Prim" { default() } else { @@ -199,9 +202,11 @@ impl ResolvedModule { } } - for import in self.qualified.values() { - if import.contains_term(file_id, item_id) { - return true; + for imports in self.qualified.values() { + for import in imports { + if import.contains_term(file_id, item_id) { + return true; + } } } @@ -215,10 +220,12 @@ impl ResolvedModule { } // if a qualified Prim import exists, use its import list; - if let Some(prim_import) = self.qualified.get("Prim") - && prim_import.contains_term(file_id, item_id) - { - return true; + if let Some(prim_imports) = self.qualified.get("Prim") { + for prim_import in prim_imports { + if prim_import.contains_term(file_id, item_id) { + return true; + } + } } // if there are no Prim imports, use the export list. @@ -231,7 +238,7 @@ impl ResolvedModule { } type ResolvedImportsUnqualified = FxHashMap>; -type ResolvedImportsQualified = FxHashMap; +type ResolvedImportsQualified = FxHashMap>; #[derive(Debug, Default, PartialEq, Eq)] pub struct ResolvedLocals { diff --git a/compiler-lsp/analyzer/src/completion/sources.rs b/compiler-lsp/analyzer/src/completion/sources.rs index 9e3ddefe..4577b11c 100644 --- a/compiler-lsp/analyzer/src/completion/sources.rs +++ b/compiler-lsp/analyzer/src/completion/sources.rs @@ -35,7 +35,8 @@ impl CompletionSource for QualifiedModules { let source = context.resolved.qualified.iter(); let source = source.filter(move |(name, _)| filter.matches(name)); - for (name, import) in source { + for (name, imports) in source { + let Some(import) = imports.first() else { continue }; let (parsed, _) = context.engine.parsed(import.file)?; let description = parsed.module_name().map(|name| name.to_string()); @@ -253,31 +254,33 @@ impl CompletionSource for QualifiedTerms<'_> { filter: F, items: &mut Vec, ) -> Result { - let Some(import) = context.resolved.qualified.get(self.0) else { + let Some(imports) = context.resolved.qualified.get(self.0) else { return Ok(()); }; - let source = import.iter_terms().filter(move |(name, _, _, kind)| { - filter.matches(name) && !matches!(kind, ImportKind::Hidden) - }); + for import in imports { + let source = import.iter_terms().filter(|(name, _, _, kind)| { + filter.matches(name) && !matches!(kind, ImportKind::Hidden) + }); - for (name, file_id, term_id, _) in source { - let (parsed, _) = context.engine.parsed(file_id)?; - let description = parsed.module_name().map(|name| name.to_string()); + for (name, file_id, term_id, _) in source { + let (parsed, _) = context.engine.parsed(file_id)?; + let description = parsed.module_name().map(|name| name.to_string()); - let mut item = CompletionItemSpec::new( - name.to_string(), - context.range, - CompletionItemKind::VALUE, - CompletionResolveData::TermItem(file_id, term_id), - ); + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::VALUE, + CompletionResolveData::TermItem(file_id, term_id), + ); - item.edit_text(format!("{}.{name}", self.0)); - if let Some(description) = description { - item.label_description(description); - } + item.edit_text(format!("{}.{name}", self.0)); + if let Some(description) = description { + item.label_description(description); + } - items.push(item.build()) + items.push(item.build()) + } } Ok(()) @@ -296,54 +299,56 @@ impl CompletionSource for QualifiedTypes<'_> { filter: F, items: &mut Vec, ) -> Result { - let Some(import) = context.resolved.qualified.get(self.0) else { + let Some(imports) = context.resolved.qualified.get(self.0) else { return Ok(()); }; - let source = import.iter_types().filter(move |(name, _, _, kind)| { - filter.matches(name) && !matches!(kind, ImportKind::Hidden) - }); + for import in imports { + let source = import.iter_types().filter(|(name, _, _, kind)| { + filter.matches(name) && !matches!(kind, ImportKind::Hidden) + }); - for (name, file_id, type_id, _) in source { - let (parsed, _) = context.engine.parsed(file_id)?; - let description = parsed.module_name().map(|name| name.to_string()); + for (name, file_id, type_id, _) in source { + let (parsed, _) = context.engine.parsed(file_id)?; + let description = parsed.module_name().map(|name| name.to_string()); - let mut item = CompletionItemSpec::new( - name.to_string(), - context.range, - CompletionItemKind::STRUCT, - CompletionResolveData::TypeItem(file_id, type_id), - ); + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(file_id, type_id), + ); - item.edit_text(format!("{}.{name}", self.0)); - if let Some(description) = description { - item.label_description(description); + item.edit_text(format!("{}.{name}", self.0)); + if let Some(description) = description { + item.label_description(description); + } + + items.push(item.build()) } - items.push(item.build()) - } + let source = import.iter_classes().filter(|(name, _, _, kind)| { + filter.matches(name) && !matches!(kind, ImportKind::Hidden) + }); - let source = import.iter_classes().filter(move |(name, _, _, kind)| { - filter.matches(name) && !matches!(kind, ImportKind::Hidden) - }); + for (name, file_id, type_id, _) in source { + let (parsed, _) = context.engine.parsed(file_id)?; + let description = parsed.module_name().map(|name| name.to_string()); - for (name, file_id, type_id, _) in source { - let (parsed, _) = context.engine.parsed(file_id)?; - let description = parsed.module_name().map(|name| name.to_string()); + let mut item = CompletionItemSpec::new( + name.to_string(), + context.range, + CompletionItemKind::STRUCT, + CompletionResolveData::TypeItem(file_id, type_id), + ); - let mut item = CompletionItemSpec::new( - name.to_string(), - context.range, - CompletionItemKind::STRUCT, - CompletionResolveData::TypeItem(file_id, type_id), - ); + item.edit_text(format!("{}.{name}", self.0)); + if let Some(description) = description { + item.label_description(description); + } - item.edit_text(format!("{}.{name}", self.0)); - if let Some(description) = description { - item.label_description(description); + items.push(item.build()) } - - items.push(item.build()) } Ok(()) @@ -727,7 +732,12 @@ fn suggestions_candidates_qualified( filter: impl Filter, items: &mut Vec, ) -> Result<(), AnalyzerError> { - let has_prim = context.resolved.qualified.values().any(|import| import.file == context.prim_id); + let has_prim = context + .resolved + .qualified + .values() + .flatten() + .any(|import| import.file == context.prim_id); let file_ids = context.files.iter_id().filter(move |&id| { let not_self = id != context.current_file; diff --git a/compiler-lsp/analyzer/src/references.rs b/compiler-lsp/analyzer/src/references.rs index 18db6f66..ee2a2649 100644 --- a/compiler-lsp/analyzer/src/references.rs +++ b/compiler-lsp/analyzer/src/references.rs @@ -423,7 +423,7 @@ fn probe_workspace_imports( let resolved = engine.resolved(workspace_file_id)?; let unqualified = resolved.unqualified.values().flatten(); - let qualified = resolved.qualified.values(); + let qualified = resolved.qualified.values().flatten(); let imports = unqualified.chain(qualified); for import in imports { @@ -447,7 +447,7 @@ fn probe_imports_for( let resolved = engine.resolved(workspace_file_id)?; let unqualified = resolved.unqualified.values().flatten(); - let qualified = resolved.qualified.values(); + let qualified = resolved.qualified.values().flatten(); let imports = unqualified.chain(qualified); for import in imports { diff --git a/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsInt.purs b/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsInt.purs new file mode 100644 index 00000000..5f808d9f --- /dev/null +++ b/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsInt.purs @@ -0,0 +1,4 @@ +module IsInt where + +value :: Int +value = 42 diff --git a/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsString.purs b/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsString.purs new file mode 100644 index 00000000..b621e6d8 --- /dev/null +++ b/tests-integration/fixtures/checking/338_module_export_alias_overlap/IsString.purs @@ -0,0 +1,7 @@ +module IsString where + +value :: String +value = "hello" + +other :: Boolean +other = true diff --git a/tests-integration/fixtures/checking/338_module_export_alias_overlap/Lib.purs b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Lib.purs new file mode 100644 index 00000000..0c49ffb8 --- /dev/null +++ b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Lib.purs @@ -0,0 +1,4 @@ +module Lib (module Exports) where + +import IsInt (value) as Exports +import IsString (other) as Exports diff --git a/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.purs b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.purs new file mode 100644 index 00000000..7c71099a --- /dev/null +++ b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Lib (value) + +test = value diff --git a/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.snap b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.snap new file mode 100644 index 00000000..20236616 --- /dev/null +++ b/tests-integration/fixtures/checking/338_module_export_alias_overlap/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Int + +Types diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap index 687e41bf..f2f03d5b 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedExplicit.snap @@ -10,10 +10,10 @@ Unqualified Imports: Qualified Imports: L Terms: - - MkLibTy is Explicit - - Ctor3 is Explicit - libFn is Explicit + - MkLibTy is Explicit - Ctor1 is Explicit + - Ctor3 is Explicit L Types: - LibTy is Explicit @@ -23,9 +23,9 @@ L Types: L Classes: Exported Terms: + - Ctor1 - libFn - MkLibTy - - Ctor1 - Ctor3 Exported Types: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap index 435d4e2a..60e4404f 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedHiding.snap @@ -10,10 +10,10 @@ Unqualified Imports: Qualified Imports: L Terms: - - MkLibTy is Implicit - - Ctor3 is Implicit - libFn is Implicit + - MkLibTy is Implicit - Ctor1 is Implicit + - Ctor3 is Implicit L Types: - LibTy is Implicit @@ -22,9 +22,9 @@ L Types: L Classes: Exported Terms: + - Ctor1 - libFn - MkLibTy - - Ctor1 - Ctor3 Exported Types: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap index ac8d6f2e..9afbb101 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportQualifiedImplicit.snap @@ -10,13 +10,13 @@ Unqualified Imports: Qualified Imports: L Terms: - - MkLibTy is Implicit - - Ctor2 is Implicit - - hideMe is Implicit - MkHideTy is Implicit - - Ctor3 is Implicit + - hideMe is Implicit - libFn is Implicit + - MkLibTy is Implicit - Ctor1 is Implicit + - Ctor2 is Implicit + - Ctor3 is Implicit L Types: - LibTy is Implicit @@ -30,9 +30,9 @@ Exported Terms: - MkHideTy - Ctor3 - libFn - - Ctor2 - MkLibTy - Ctor1 + - Ctor2 - hideMe Exported Types: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap index ad768136..e15208cf 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedExplicit.snap @@ -8,10 +8,10 @@ module ImportUnqualifiedExplicit Unqualified Imports: Terms: - - MkLibTy is Explicit - - Ctor3 is Explicit - libFn is Explicit + - MkLibTy is Explicit - Ctor1 is Explicit + - Ctor3 is Explicit Types: - LibTy is Explicit @@ -23,9 +23,9 @@ Classes: Qualified Imports: Exported Terms: + - Ctor1 - libFn - MkLibTy - - Ctor1 - Ctor3 Exported Types: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap index bdfabdb3..7d1a7cda 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedHiding.snap @@ -8,10 +8,10 @@ module ImportUnqualifiedHiding Unqualified Imports: Terms: - - MkLibTy is Implicit - - Ctor3 is Implicit - libFn is Implicit + - MkLibTy is Implicit - Ctor1 is Implicit + - Ctor3 is Implicit Types: - LibTy is Implicit @@ -22,9 +22,9 @@ Classes: Qualified Imports: Exported Terms: + - Ctor1 - libFn - MkLibTy - - Ctor1 - Ctor3 Exported Types: diff --git a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap index 78556a04..d26c0eff 100644 --- a/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap +++ b/tests-integration/fixtures/resolving/002_import_resolution/ImportUnqualifiedImplicit.snap @@ -8,13 +8,13 @@ module ImportUnqualifiedImplicit Unqualified Imports: Terms: - - MkLibTy is Implicit - - Ctor2 is Implicit - - hideMe is Implicit - MkHideTy is Implicit - - Ctor3 is Implicit + - hideMe is Implicit - libFn is Implicit + - MkLibTy is Implicit - Ctor1 is Implicit + - Ctor2 is Implicit + - Ctor3 is Implicit Types: - LibTy is Implicit @@ -30,9 +30,9 @@ Exported Terms: - MkHideTy - Ctor3 - libFn - - Ctor2 - MkLibTy - Ctor1 + - Ctor2 - hideMe Exported Types: diff --git a/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap b/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap index a217552b..306eb304 100644 --- a/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap +++ b/tests-integration/fixtures/resolving/003_import_errors/DuplicateQualifiedImport.snap @@ -9,6 +9,14 @@ Unqualified Imports: Qualified Imports: +Library Terms: + - html is Implicit + +Library Types: + - Html is Implicit + +Library Classes: + Library Terms: - html is Implicit @@ -33,5 +41,3 @@ Local Classes: Class Members: Errors: - - TermImportConflict { existing: (Idx::(14), Idx::(0), AstId(9)), duplicate: (Idx::(13), Idx::(0), AstId(5)) } - - TypeImportConflict { existing: (Idx::(14), Idx::(0), AstId(9)), duplicate: (Idx::(13), Idx::(0), AstId(5)) } diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index e3b455db..21abe995 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -51,32 +51,34 @@ pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { writeln!(buffer).unwrap(); writeln!(buffer, "Qualified Imports:").unwrap(); - for (name, import) in &resolved.qualified { - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Terms:").unwrap(); - for (name, _, _, kind) in import.iter_terms() { - if matches!(kind, ImportKind::Hidden) { - continue; + for (name, imports) in &resolved.qualified { + for import in imports { + writeln!(buffer).unwrap(); + writeln!(buffer, "{name} Terms:").unwrap(); + for (name, _, _, kind) in import.iter_terms() { + if matches!(kind, ImportKind::Hidden) { + continue; + } + writeln!(buffer, " - {name} is {kind:?}").unwrap(); } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Types:").unwrap(); - for (name, _, _, kind) in import.iter_types() { - if matches!(kind, ImportKind::Hidden) { - continue; + writeln!(buffer).unwrap(); + writeln!(buffer, "{name} Types:").unwrap(); + for (name, _, _, kind) in import.iter_types() { + if matches!(kind, ImportKind::Hidden) { + continue; + } + writeln!(buffer, " - {name} is {kind:?}").unwrap(); } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Classes:").unwrap(); - for (name, _, _, kind) in import.iter_classes() { - if matches!(kind, ImportKind::Hidden) { - continue; + writeln!(buffer).unwrap(); + writeln!(buffer, "{name} Classes:").unwrap(); + for (name, _, _, kind) in import.iter_classes() { + if matches!(kind, ImportKind::Hidden) { + continue; + } + writeln!(buffer, " - {name} is {kind:?}").unwrap(); } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); } } diff --git a/tests-integration/tests/checking/generated.rs b/tests-integration/tests/checking/generated.rs index 6b691d14..c21be9b4 100644 --- a/tests-integration/tests/checking/generated.rs +++ b/tests-integration/tests/checking/generated.rs @@ -687,3 +687,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_336_row_open_record_main() { run_test("336_row_open_record", "Main"); } #[rustfmt::skip] #[test] fn test_337_void_data_main() { run_test("337_void_data", "Main"); } + +#[rustfmt::skip] #[test] fn test_338_module_export_alias_overlap_main() { run_test("338_module_export_alias_overlap", "Main"); } From f8cfc6bdf42391d520b6c327250883e1411cdd93 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 14 Feb 2026 22:57:09 +0800 Subject: [PATCH 173/386] Expand type synonyms in Prim.Row classes --- .../checking/src/algorithm/constraint.rs | 8 +- .../constraint/compiler_solved/prim_row.rs | 102 ++++++++++-------- 2 files changed, 63 insertions(+), 47 deletions(-) diff --git a/compiler-core/checking/src/algorithm/constraint.rs b/compiler-core/checking/src/algorithm/constraint.rs index 0d7ad934..21f5f89d 100644 --- a/compiler-core/checking/src/algorithm/constraint.rs +++ b/compiler-core/checking/src/algorithm/constraint.rs @@ -1209,10 +1209,10 @@ where cons => prim_symbol_cons(state, arguments), }, context.prim_row => { - union => prim_row_union(state, context, arguments), - cons => prim_row_cons(state, arguments), - lacks => prim_row_lacks(state, context, arguments), - nub => prim_row_nub(state, arguments), + union => prim_row_union(state, context, arguments)?, + cons => prim_row_cons(state, context, arguments)?, + lacks => prim_row_lacks(state, context, arguments)?, + nub => prim_row_nub(state, context, arguments)?, }, context.prim_row_list => { row_to_list => prim_rowlist_row_to_list(state, context, arguments), diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs index 3ab8ff8e..a6c1b310 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs @@ -1,11 +1,13 @@ use std::cmp::Ordering; use std::iter; +use building_types::QueryResult; use rustc_hash::FxHashSet; use crate::ExternalQueries; use crate::algorithm::constraint::{self, MatchInstance}; use crate::algorithm::state::{CheckContext, CheckState}; +use crate::algorithm::toolkit; use crate::core::{RowField, RowType}; use crate::{Type, TypeId}; @@ -70,17 +72,17 @@ pub fn prim_row_union( state: &mut CheckState, context: &CheckContext, arguments: &[TypeId], -) -> Option +) -> QueryResult> where Q: ExternalQueries, { let &[left, right, union] = arguments else { - return None; + return Ok(None); }; - let left = state.normalize_type(left); - let right = state.normalize_type(right); - let union = state.normalize_type(union); + let left = toolkit::normalise_expand_type(state, context, left)?; + let right = toolkit::normalise_expand_type(state, context, right)?; + let union = toolkit::normalise_expand_type(state, context, union)?; let left_row = extract_row(state, left); let right_row = extract_row(state, right); @@ -90,7 +92,7 @@ where (Some(left_row), Some(right_row), _) => { if let Some(rest) = left_row.tail { if left_row.fields.is_empty() { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); } let fresh_tail = state.fresh_unification_kinded(context.prim.row_type); @@ -109,10 +111,10 @@ where let constraint = state.storage.intern(Type::Application(constraint, right)); let constraint = state.storage.intern(Type::Application(constraint, fresh_tail)); - return Some(MatchInstance::Match { + return Ok(Some(MatchInstance::Match { constraints: vec![constraint], equalities: vec![(union, result)], - }); + })); } let union_fields = { @@ -125,11 +127,11 @@ where .storage .intern(Type::Row(RowType::from_unsorted(union_fields, right_row.tail))); - Some(MatchInstance::Match { constraints: vec![], equalities: vec![(union, result)] }) + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![(union, result)] })) } (_, Some(right_row), Some(union_row)) => { if right_row.tail.is_some() { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); } if let Some((remaining, mut equalities)) = subtract_row_fields(state, &union_row.fields, &right_row.fields) @@ -138,14 +140,14 @@ where .storage .intern(Type::Row(RowType::from_unsorted(remaining, union_row.tail))); equalities.push((left, result)); - Some(MatchInstance::Match { constraints: vec![], equalities }) + Ok(Some(MatchInstance::Match { constraints: vec![], equalities })) } else { - Some(MatchInstance::Apart) + Ok(Some(MatchInstance::Apart)) } } (Some(left_row), _, Some(union_row)) => { if left_row.tail.is_some() { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); } if let Some((remaining, mut equalities)) = subtract_row_fields(state, &union_row.fields, &left_row.fields) @@ -154,24 +156,31 @@ where .storage .intern(Type::Row(RowType::from_unsorted(remaining, union_row.tail))); equalities.push((right, result)); - Some(MatchInstance::Match { constraints: vec![], equalities }) + Ok(Some(MatchInstance::Match { constraints: vec![], equalities })) } else { - Some(MatchInstance::Apart) + Ok(Some(MatchInstance::Apart)) } } - _ => Some(MatchInstance::Stuck), + _ => Ok(Some(MatchInstance::Stuck)), } } -pub fn prim_row_cons(state: &mut CheckState, arguments: &[TypeId]) -> Option { +pub fn prim_row_cons( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ let &[label, a, tail, row] = arguments else { - return None; + return Ok(None); }; - let label = state.normalize_type(label); - let a = state.normalize_type(a); - let tail = state.normalize_type(tail); - let row = state.normalize_type(row); + let label = toolkit::normalise_expand_type(state, context, label)?; + let a = toolkit::normalise_expand_type(state, context, a)?; + let tail = toolkit::normalise_expand_type(state, context, tail)?; + let row = toolkit::normalise_expand_type(state, context, row)?; let label_symbol = extract_symbol(state, label); let tail_row = extract_row(state, tail); @@ -185,7 +194,7 @@ pub fn prim_row_cons(state: &mut CheckState, arguments: &[TypeId]) -> Option { @@ -204,15 +213,15 @@ pub fn prim_row_cons(state: &mut CheckState, arguments: &[TypeId]) -> Option Some(MatchInstance::Stuck), + _ => Ok(Some(MatchInstance::Stuck)), } } @@ -220,32 +229,32 @@ pub fn prim_row_lacks( state: &mut CheckState, context: &CheckContext, arguments: &[TypeId], -) -> Option +) -> QueryResult> where Q: ExternalQueries, { let &[label, row] = arguments else { - return None; + return Ok(None); }; - let label = state.normalize_type(label); - let row = state.normalize_type(row); + let label = toolkit::normalise_expand_type(state, context, label)?; + let row = toolkit::normalise_expand_type(state, context, row)?; let Some(label_value) = extract_symbol(state, label) else { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); }; let Some(row_row) = extract_row(state, row) else { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); }; let has_label = row_row.fields.iter().any(|field| field.label == label_value); if has_label { - Some(MatchInstance::Apart) + Ok(Some(MatchInstance::Apart)) } else if let Some(tail) = row_row.tail { if row_row.fields.is_empty() { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); } let prim_row = &context.prim_row; @@ -254,22 +263,29 @@ where let constraint = state.storage.intern(Type::Application(constraint, label)); let constraint = state.storage.intern(Type::Application(constraint, tail)); - Some(MatchInstance::Match { constraints: vec![constraint], equalities: vec![] }) + Ok(Some(MatchInstance::Match { constraints: vec![constraint], equalities: vec![] })) } else { - Some(MatchInstance::Match { constraints: vec![], equalities: vec![] }) + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })) } } -pub fn prim_row_nub(state: &mut CheckState, arguments: &[TypeId]) -> Option { +pub fn prim_row_nub( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ let &[original, nubbed] = arguments else { - return None; + return Ok(None); }; - let original = state.normalize_type(original); - let nubbed = state.normalize_type(nubbed); + let original = toolkit::normalise_expand_type(state, context, original)?; + let nubbed = toolkit::normalise_expand_type(state, context, nubbed)?; let Some(original_row) = extract_closed_row(state, original) else { - return Some(MatchInstance::Stuck); + return Ok(Some(MatchInstance::Stuck)); }; let mut seen = FxHashSet::default(); @@ -282,5 +298,5 @@ pub fn prim_row_nub(state: &mut CheckState, arguments: &[TypeId]) -> Option Date: Wed, 18 Feb 2026 03:40:30 +0800 Subject: [PATCH 174/386] Format files --- compiler-compatibility/command/src/compat.rs | 3 +-- .../src/algorithm/constraint/compiler_solved/prim_row.rs | 8 +++++--- compiler-lsp/analyzer/src/completion/sources.rs | 8 ++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 06975d09..c89baa2e 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -11,9 +11,8 @@ use petgraph::graphmap::DiGraphMap; use rayon::prelude::*; use url::Url; -use crate::loader; -use crate::resolver; use crate::types::ResolvedSet; +use crate::{loader, resolver}; /// Result of checking a single file. pub struct FileResult { diff --git a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs index a6c1b310..77d40d0d 100644 --- a/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs +++ b/compiler-core/checking/src/algorithm/constraint/compiler_solved/prim_row.rs @@ -4,12 +4,11 @@ use std::iter; use building_types::QueryResult; use rustc_hash::FxHashSet; -use crate::ExternalQueries; use crate::algorithm::constraint::{self, MatchInstance}; use crate::algorithm::state::{CheckContext, CheckState}; use crate::algorithm::toolkit; use crate::core::{RowField, RowType}; -use crate::{Type, TypeId}; +use crate::{ExternalQueries, Type, TypeId}; use super::extract_symbol; @@ -127,7 +126,10 @@ where .storage .intern(Type::Row(RowType::from_unsorted(union_fields, right_row.tail))); - Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![(union, result)] })) + Ok(Some(MatchInstance::Match { + constraints: vec![], + equalities: vec![(union, result)], + })) } (_, Some(right_row), Some(union_row)) => { if right_row.tail.is_some() { diff --git a/compiler-lsp/analyzer/src/completion/sources.rs b/compiler-lsp/analyzer/src/completion/sources.rs index 4577b11c..d87d8489 100644 --- a/compiler-lsp/analyzer/src/completion/sources.rs +++ b/compiler-lsp/analyzer/src/completion/sources.rs @@ -732,12 +732,8 @@ fn suggestions_candidates_qualified( filter: impl Filter, items: &mut Vec, ) -> Result<(), AnalyzerError> { - let has_prim = context - .resolved - .qualified - .values() - .flatten() - .any(|import| import.file == context.prim_id); + let has_prim = + context.resolved.qualified.values().flatten().any(|import| import.file == context.prim_id); let file_ids = context.files.iter_id().filter(move |&id| { let not_self = id != context.current_file; From f418652bafff22185b419835756537d604ead197 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 17 Feb 2026 15:21:17 +0800 Subject: [PATCH 175/386] Add core type structures --- Cargo.lock | 11 +++ compiler-core/checking2/Cargo.toml | 11 +++ compiler-core/checking2/src/core.rs | 117 ++++++++++++++++++++++++++++ compiler-core/checking2/src/lib.rs | 1 + 4 files changed, 140 insertions(+) create mode 100644 compiler-core/checking2/Cargo.toml create mode 100644 compiler-core/checking2/src/core.rs create mode 100644 compiler-core/checking2/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8850f009..c6cc2140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,6 +325,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "checking2" +version = "0.1.0" +dependencies = [ + "files", + "indexing", + "interner", + "lowering", + "smol_str", +] + [[package]] name = "chrono" version = "0.4.43" diff --git a/compiler-core/checking2/Cargo.toml b/compiler-core/checking2/Cargo.toml new file mode 100644 index 00000000..a187c02a --- /dev/null +++ b/compiler-core/checking2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "checking2" +version = "0.1.0" +edition = "2024" + +[dependencies] +files = { version = "0.1.0", path = "../files" } +indexing = { version = "0.1.0", path = "../indexing" } +interner = { version = "0.1.0", path = "../interner" } +lowering = { version = "0.1.0", path = "../lowering" } +smol_str = "0.3.5" diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs new file mode 100644 index 00000000..22f1a8ca --- /dev/null +++ b/compiler-core/checking2/src/core.rs @@ -0,0 +1,117 @@ +//! Implements core type structures. + +use std::sync::Arc; + +use files::FileId; +use indexing::TypeItemId; +use smol_str::SmolStr; + +/// A globally unique identity for a rigid type variable. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Name { + pub file: FileId, + pub unique: u32, +} + +/// A marker used to represent binding levels of variables. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Depth(pub u32); + +/// Carries information about a type variable under a forall. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ForallBinder { + /// Whether this binder is visible to type applications. + pub visible: bool, + /// The unique identity attached to the type variable. + pub name: Name, + /// The kind of the type variable. + pub kind: TypeId, +} + +/// Represents a row type. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RowType { + /// A stable-sorted list representing `Map>`. + pub fields: Arc<[RowField]>, + /// Closed row if [`None`]; Open row if [`Some`]. + pub tail: Option, +} + +/// A field in a row type. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RowField { + /// The label of the row field. + pub label: SmolStr, + /// The [`Type`] of the row field. + pub id: TypeId, +} + +/// The saturation of a synonym application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Saturation { + /// Fully applied synonym. + Full, + /// Partially applied synonym. + Partial, +} + +/// Represents a type synonym. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Synonym { + /// Whether fully or partially applied. + pub saturation: Saturation, + /// The reference to the synonym type. + pub reference: (FileId, TypeItemId), + /// Arguments to the synonym constructor. + pub arguments: Arc<[TypeId]>, +} + +/// The core type representation used by the checker after name resolution. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Type { + /// Type application, `Array Int`. + Application(TypeId, TypeId), + /// Kind application, `Proxy @Int`. + KindApplication(TypeId, TypeId), + /// Binary type operator application, `Fields + ()`. + OperatorApplication(FileId, TypeItemId, TypeId, TypeId), + /// Type synonym application, see [`Synonym`]. + SynonymApplication(SynonymId), + + /// A universally quantified type, `forall a. a -> a`. + Forall(ForallBinderId, TypeId), + /// A constrained type, `Constraint => Constrained`. + Constrained(TypeId, TypeId), + /// A function type, `a -> b`. + Function(TypeId, TypeId), + /// A type with an explicit kind, `T :: K`. + Kinded(TypeId, TypeId), + + /// A resolved type constructor reference. + Constructor(FileId, TypeItemId), + /// A resolved type operator reference. + OperatorConstructor(FileId, TypeItemId), + + /// A type-level integer literal, `42`. + Integer(i32), + /// A type-level string literal, `"life"`. + String(lowering::StringKind, SmolStrId), + /// A row type, see [`RowType`]. + Row(RowTypeId), + + /// A bound skolem variable that can only unify with itself. + Rigid(Name, TypeId), + /// A unification variable that can be solved to another [`Type`]. + Unification(u32), + + /// A type variable that did not resolve to a binder. + Free(SmolStrId), + /// Recovery marker for exceptional type checker paths. + Unknown(SmolStrId), +} + +pub type ForallBinderId = interner::Id; +pub type RowTypeId = interner::Id; +pub type SynonymId = interner::Id; +pub type SmolStrId = interner::Id; +pub type TypeId = interner::Id; diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs new file mode 100644 index 00000000..5a7ca06a --- /dev/null +++ b/compiler-core/checking2/src/lib.rs @@ -0,0 +1 @@ +pub mod core; From 37eca573aa430d6ed057d231eb79854ef65a1e90 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 17 Feb 2026 23:18:33 +0800 Subject: [PATCH 176/386] Add checking2 crate with ExternalQueries, CheckedModule, and check_prim Co-authored-by: Amp --- Cargo.lock | 6 ++ compiler-core/checking2/Cargo.toml | 6 ++ compiler-core/checking2/src/core.rs | 8 ++ compiler-core/checking2/src/kind.rs | 1 + compiler-core/checking2/src/lib.rs | 145 ++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+) create mode 100644 compiler-core/checking2/src/kind.rs diff --git a/Cargo.lock b/Cargo.lock index c6cc2140..21493b60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,11 +329,17 @@ dependencies = [ name = "checking2" version = "0.1.0" dependencies = [ + "building-types", "files", "indexing", "interner", "lowering", + "parsing", + "resolving", + "rustc-hash 2.1.1", "smol_str", + "stabilizing", + "sugar", ] [[package]] diff --git a/compiler-core/checking2/Cargo.toml b/compiler-core/checking2/Cargo.toml index a187c02a..2c9c98d1 100644 --- a/compiler-core/checking2/Cargo.toml +++ b/compiler-core/checking2/Cargo.toml @@ -4,8 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] +building-types = { version = "0.1.0", path = "../building-types" } files = { version = "0.1.0", path = "../files" } indexing = { version = "0.1.0", path = "../indexing" } interner = { version = "0.1.0", path = "../interner" } lowering = { version = "0.1.0", path = "../lowering" } +parsing = { version = "0.1.0", path = "../parsing" } +resolving = { version = "0.1.0", path = "../resolving" } +rustc-hash = "2.1.1" smol_str = "0.3.5" +stabilizing = { version = "0.1.0", path = "../stabilizing" } +sugar = { version = "0.1.0", path = "../sugar" } diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 22f1a8ca..96e9fb54 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -110,6 +110,14 @@ pub enum Type { Unknown(SmolStrId), } +/// The role of a type parameter for safe coercions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Role { + Phantom, + Representational, + Nominal, +} + pub type ForallBinderId = interner::Id; pub type RowTypeId = interner::Id; pub type SynonymId = interner::Id; diff --git a/compiler-core/checking2/src/kind.rs b/compiler-core/checking2/src/kind.rs new file mode 100644 index 00000000..bea8095c --- /dev/null +++ b/compiler-core/checking2/src/kind.rs @@ -0,0 +1 @@ +//! Syntax-driven rules for kind checking. diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 5a7ca06a..abfe95fc 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1 +1,146 @@ pub mod core; +pub mod kind; + +use std::sync::Arc; + +use building_types::{QueryProxy, QueryResult}; +use files::FileId; +use indexing::TypeItemId; +use resolving::ResolvedModule; +use rustc_hash::FxHashMap; +use smol_str::SmolStr; + +use crate::core::{ + ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, +}; + +pub trait ExternalQueries: + QueryProxy< + Parsed = parsing::FullParsedModule, + Stabilized = Arc, + Indexed = Arc, + Lowered = Arc, + Grouped = Arc, + Resolved = Arc, + Bracketed = Arc, + Sectioned = Arc, + Checked = Arc, + > +{ + fn intern_type(&self, t: Type) -> TypeId; + fn lookup_type(&self, id: TypeId) -> Type; + + fn intern_forall_binder(&self, b: ForallBinder) -> ForallBinderId; + fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder; + + fn intern_row_type(&self, r: RowType) -> RowTypeId; + fn lookup_row_type(&self, id: RowTypeId) -> RowType; + + fn intern_synonym(&self, s: Synonym) -> SynonymId; + fn lookup_synonym(&self, id: SynonymId) -> Synonym; + + fn intern_smol_str(&self, s: SmolStr) -> core::SmolStrId; + fn lookup_smol_str(&self, id: core::SmolStrId) -> SmolStr; +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct CheckedModule { + pub types: FxHashMap, + pub roles: FxHashMap>, +} + +impl CheckedModule { + pub fn lookup_type(&self, id: TypeItemId) -> Option { + self.types.get(&id).copied() + } + + pub fn lookup_roles(&self, id: TypeItemId) -> Option> { + self.roles.get(&id).cloned() + } +} + +pub fn check_module(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { + let prim_id = queries.prim_id(); + if file_id == prim_id { + check_prim(queries, prim_id) + } else { + Ok(CheckedModule::default()) + } +} + +fn check_prim(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { + let mut checked = CheckedModule::default(); + let resolved = queries.resolved(file_id)?; + + let lookup_type = |name: &str| { + let prim_type = resolved.exports.lookup_type(name); + prim_type.unwrap_or_else(|| unreachable!("invariant violated: {name} not in Prim")) + }; + + let lookup_class = |name: &str| { + let prim_class = resolved.exports.lookup_class(name); + prim_class.unwrap_or_else(|| unreachable!("invariant violated: {name} not in Prim")) + }; + + let type_core = { + let (file_id, item_id) = lookup_type("Type"); + queries.intern_type(Type::Constructor(file_id, item_id)) + }; + + let row_core = { + let (file_id, item_id) = lookup_type("Row"); + queries.intern_type(Type::Constructor(file_id, item_id)) + }; + + let constraint_core = { + let (file_id, item_id) = lookup_type("Constraint"); + queries.intern_type(Type::Constructor(file_id, item_id)) + }; + + let type_to_type = queries.intern_type(Type::Function(type_core, type_core)); + let function_kind = queries.intern_type(Type::Function(type_core, type_to_type)); + + let row_type = queries.intern_type(Type::Application(row_core, type_core)); + let record_kind = queries.intern_type(Type::Function(row_type, type_core)); + + let mut insert_type = |name: &str, id: TypeId| { + let (_, item_id) = lookup_type(name); + checked.types.insert(item_id, id); + }; + + insert_type("Type", type_core); + insert_type("Function", function_kind); + insert_type("Array", type_to_type); + insert_type("Record", record_kind); + insert_type("Number", type_core); + insert_type("Int", type_core); + insert_type("String", type_core); + insert_type("Char", type_core); + insert_type("Boolean", type_core); + insert_type("Constraint", type_core); + insert_type("Symbol", type_core); + insert_type("Row", type_to_type); + + let (_, partial_id) = lookup_class("Partial"); + checked.types.insert(partial_id, constraint_core); + + let mut insert_roles = |name: &str, roles: &[Role]| { + let (_, item_id) = lookup_type(name); + checked.roles.insert(item_id, Arc::from(roles)); + }; + + insert_roles("Type", &[]); + insert_roles("Function", &[Role::Representational, Role::Representational]); + insert_roles("Array", &[Role::Representational]); + insert_roles("Record", &[Role::Representational]); + insert_roles("Number", &[]); + insert_roles("Int", &[]); + insert_roles("String", &[]); + insert_roles("Char", &[]); + insert_roles("Boolean", &[]); + insert_roles("Constraint", &[]); + insert_roles("Symbol", &[]); + insert_roles("Row", &[Role::Representational]); + + Ok(checked) +} From 7a2ce63b1bfcbdc2d16be2857a40e114c5d88dd7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 17 Feb 2026 23:59:44 +0800 Subject: [PATCH 177/386] Add context module with CheckContext and caches for known types Co-authored-by: Amp --- compiler-core/checking2/src/context.rs | 588 +++++++++++++++++++++++++ compiler-core/checking2/src/lib.rs | 1 + 2 files changed, 589 insertions(+) create mode 100644 compiler-core/checking2/src/context.rs diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs new file mode 100644 index 00000000..798b9f6f --- /dev/null +++ b/compiler-core/checking2/src/context.rs @@ -0,0 +1,588 @@ +//! Read-only environment for the type checking algorithm. +//! +//! See documentation for [`CheckContext`] for more information. + +use std::sync::Arc; + +use building_types::QueryResult; +use files::FileId; +use indexing::{IndexedModule, TermItemId, TypeItemId}; +use lowering::{GroupedModule, LoweredModule}; +use resolving::ResolvedModule; +use stabilizing::StabilizedModule; +use sugar::{Bracketed, Sectioned}; + +use crate::ExternalQueries; +use crate::core::{Type, TypeId}; + +/// The read-only environment threaded through the type checking algorithm. +/// +/// This structure holds a reference to [`ExternalQueries`] for interning and +/// making build system queries; Arc references to query results for the current +/// module; and cached lookups for modules and items 'known' by the compiler. +pub struct CheckContext<'q, Q> +where + Q: ExternalQueries, +{ + pub queries: &'q Q, + + pub prim: PrimCore, + pub prim_int: PrimIntCore, + pub prim_boolean: PrimBooleanCore, + pub prim_ordering: PrimOrderingCore, + pub prim_symbol: PrimSymbolCore, + pub prim_row: PrimRowCore, + pub prim_row_list: PrimRowListCore, + pub prim_coerce: PrimCoerceCore, + pub prim_type_error: PrimTypeErrorCore, + pub known_types: KnownTypesCore, + pub known_terms: KnownTermsCore, + pub known_reflectable: KnownReflectableCore, + pub known_generic: Option, + + pub id: FileId, + pub stabilized: Arc, + pub indexed: Arc, + pub lowered: Arc, + pub grouped: Arc, + pub bracketed: Arc, + pub sectioned: Arc, + pub resolved: Arc, + + pub prim_indexed: Arc, + pub prim_resolved: Arc, +} + +impl<'q, Q> CheckContext<'q, Q> +where + Q: ExternalQueries, +{ + pub fn new(queries: &'q Q, id: FileId) -> QueryResult> { + let stabilized = queries.stabilized(id)?; + let indexed = queries.indexed(id)?; + let lowered = queries.lowered(id)?; + let grouped = queries.grouped(id)?; + let bracketed = queries.bracketed(id)?; + let sectioned = queries.sectioned(id)?; + let resolved = queries.resolved(id)?; + + let prim = PrimCore::collect(queries)?; + let prim_int = PrimIntCore::collect(queries)?; + let prim_boolean = PrimBooleanCore::collect(queries)?; + let prim_ordering = PrimOrderingCore::collect(queries)?; + let prim_symbol = PrimSymbolCore::collect(queries)?; + let prim_row = PrimRowCore::collect(queries)?; + let prim_row_list = PrimRowListCore::collect(queries)?; + let prim_coerce = PrimCoerceCore::collect(queries)?; + let prim_type_error = PrimTypeErrorCore::collect(queries)?; + let known_types = KnownTypesCore::collect(queries)?; + let known_terms = KnownTermsCore::collect(queries)?; + let known_reflectable = KnownReflectableCore::collect(queries)?; + let known_generic = KnownGeneric::collect(queries)?; + + let prim_id = queries.prim_id(); + let prim_indexed = queries.indexed(prim_id)?; + let prim_resolved = queries.resolved(prim_id)?; + + Ok(CheckContext { + queries, + prim, + prim_int, + prim_boolean, + prim_ordering, + prim_symbol, + prim_row, + prim_row_list, + prim_coerce, + prim_type_error, + known_types, + known_terms, + known_reflectable, + known_generic, + id, + stabilized, + indexed, + lowered, + grouped, + bracketed, + sectioned, + resolved, + prim_indexed, + prim_resolved, + }) + } +} + +struct PrimLookup<'r, 'q, Q: ExternalQueries> { + resolved: &'r ResolvedModule, + queries: &'q Q, + module_name: &'static str, +} + +impl<'r, 'q, Q: ExternalQueries> PrimLookup<'r, 'q, Q> { + fn new(resolved: &'r ResolvedModule, queries: &'q Q, module_name: &'static str) -> Self { + PrimLookup { resolved, queries, module_name } + } + + fn type_item(&self, name: &str) -> TypeItemId { + let (_, type_id) = self.resolved.exports.lookup_type(name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + type_id + } + + fn type_constructor(&self, name: &str) -> TypeId { + let (file_id, type_id) = self.resolved.exports.lookup_type(name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + self.queries.intern_type(Type::Constructor(file_id, type_id)) + } + + fn class_item(&self, name: &str) -> TypeItemId { + let (_, type_id) = self.resolved.exports.lookup_class(name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + type_id + } + + fn class_constructor(&self, name: &str) -> TypeId { + let (file_id, type_id) = self.resolved.exports.lookup_class(name).unwrap_or_else(|| { + unreachable!("invariant violated: {name} not in {}", self.module_name) + }); + self.queries.intern_type(Type::Constructor(file_id, type_id)) + } + + fn intern(&self, ty: Type) -> TypeId { + self.queries.intern_type(ty) + } +} + +pub struct PrimCore { + pub prim_id: FileId, + pub t: TypeId, + pub type_to_type: TypeId, + pub function: TypeId, + pub function_item: TypeItemId, + pub array: TypeId, + pub record: TypeId, + pub number: TypeId, + pub int: TypeId, + pub string: TypeId, + pub char: TypeId, + pub boolean: TypeId, + pub partial: TypeId, + pub constraint: TypeId, + pub symbol: TypeId, + pub row: TypeId, + pub row_type: TypeId, +} + +impl PrimCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let prim_id = queries.prim_id(); + let resolved = queries.resolved(prim_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim"); + + let t = lookup.type_constructor("Type"); + let type_to_type = lookup.intern(Type::Function(t, t)); + + let row = lookup.type_constructor("Row"); + let row_type = lookup.intern(Type::Application(row, t)); + + let function = lookup.type_constructor("Function"); + let function_item = lookup.type_item("Function"); + + Ok(PrimCore { + prim_id, + t, + type_to_type, + function, + function_item, + array: lookup.type_constructor("Array"), + record: lookup.type_constructor("Record"), + number: lookup.type_constructor("Number"), + int: lookup.type_constructor("Int"), + string: lookup.type_constructor("String"), + char: lookup.type_constructor("Char"), + boolean: lookup.type_constructor("Boolean"), + partial: lookup.class_constructor("Partial"), + constraint: lookup.type_constructor("Constraint"), + symbol: lookup.type_constructor("Symbol"), + row, + row_type, + }) + } +} + +pub struct PrimIntCore { + pub file_id: FileId, + pub add: TypeItemId, + pub mul: TypeItemId, + pub compare: TypeItemId, + pub to_string: TypeItemId, +} + +impl PrimIntCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Int") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Int not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.Int"); + + Ok(PrimIntCore { + file_id, + add: lookup.class_item("Add"), + mul: lookup.class_item("Mul"), + compare: lookup.class_item("Compare"), + to_string: lookup.class_item("ToString"), + }) + } +} + +pub struct PrimBooleanCore { + pub true_: TypeId, + pub false_: TypeId, +} + +impl PrimBooleanCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Boolean") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Boolean not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.Boolean"); + + Ok(PrimBooleanCore { + true_: lookup.type_constructor("True"), + false_: lookup.type_constructor("False"), + }) + } +} + +pub struct PrimOrderingCore { + pub lt: TypeId, + pub eq: TypeId, + pub gt: TypeId, +} + +impl PrimOrderingCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Ordering") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Ordering not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.Ordering"); + + Ok(PrimOrderingCore { + lt: lookup.type_constructor("LT"), + eq: lookup.type_constructor("EQ"), + gt: lookup.type_constructor("GT"), + }) + } +} + +pub struct PrimSymbolCore { + pub file_id: FileId, + pub append: TypeItemId, + pub compare: TypeItemId, + pub cons: TypeItemId, +} + +impl PrimSymbolCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Symbol") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Symbol not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.Symbol"); + + Ok(PrimSymbolCore { + file_id, + append: lookup.class_item("Append"), + compare: lookup.class_item("Compare"), + cons: lookup.class_item("Cons"), + }) + } +} + +pub struct PrimRowCore { + pub file_id: FileId, + pub union: TypeItemId, + pub cons: TypeItemId, + pub lacks: TypeItemId, + pub nub: TypeItemId, +} + +impl PrimRowCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Row") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Row not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.Row"); + + Ok(PrimRowCore { + file_id, + union: lookup.class_item("Union"), + cons: lookup.class_item("Cons"), + lacks: lookup.class_item("Lacks"), + nub: lookup.class_item("Nub"), + }) + } +} + +pub struct PrimRowListCore { + pub file_id: FileId, + pub row_to_list: TypeItemId, + pub cons: TypeId, + pub nil: TypeId, +} + +impl PrimRowListCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.RowList") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.RowList not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.RowList"); + + Ok(PrimRowListCore { + file_id, + row_to_list: lookup.class_item("RowToList"), + cons: lookup.type_constructor("Cons"), + nil: lookup.type_constructor("Nil"), + }) + } +} + +pub struct PrimCoerceCore { + pub file_id: FileId, + pub coercible: TypeItemId, +} + +impl PrimCoerceCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.Coerce") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.Coerce not found")); + + let resolved = queries.resolved(file_id)?; + let (_, coercible) = resolved + .exports + .lookup_class("Coercible") + .unwrap_or_else(|| unreachable!("invariant violated: Coercible not in Prim.Coerce")); + + Ok(PrimCoerceCore { file_id, coercible }) + } +} + +pub struct PrimTypeErrorCore { + pub file_id: FileId, + pub warn: TypeItemId, + pub fail: TypeItemId, + pub text: TypeId, + pub quote: TypeId, + pub quote_label: TypeId, + pub beside: TypeId, + pub above: TypeId, +} + +impl PrimTypeErrorCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let file_id = queries + .module_file("Prim.TypeError") + .unwrap_or_else(|| unreachable!("invariant violated: Prim.TypeError not found")); + + let resolved = queries.resolved(file_id)?; + let lookup = PrimLookup::new(&resolved, queries, "Prim.TypeError"); + + Ok(PrimTypeErrorCore { + file_id, + warn: lookup.class_item("Warn"), + fail: lookup.class_item("Fail"), + text: lookup.type_constructor("Text"), + quote: lookup.type_constructor("Quote"), + quote_label: lookup.type_constructor("QuoteLabel"), + beside: lookup.type_constructor("Beside"), + above: lookup.type_constructor("Above"), + }) + } +} + +fn fetch_known_term( + queries: &impl ExternalQueries, + m: &str, + n: &str, +) -> QueryResult> { + let Some(file_id) = queries.module_file(m) else { + return Ok(None); + }; + let resolved = queries.resolved(file_id)?; + let Some((file_id, term_id)) = resolved.exports.lookup_term(n) else { + return Ok(None); + }; + Ok(Some((file_id, term_id))) +} + +fn fetch_known_class( + queries: &impl ExternalQueries, + m: &str, + n: &str, +) -> QueryResult> { + let Some(file_id) = queries.module_file(m) else { + return Ok(None); + }; + let resolved = queries.resolved(file_id)?; + let Some((file_id, type_id)) = resolved.exports.lookup_class(n) else { + return Ok(None); + }; + Ok(Some((file_id, type_id))) +} + +fn fetch_known_constructor( + queries: &impl ExternalQueries, + m: &str, + n: &str, +) -> QueryResult> { + let Some(file_id) = queries.module_file(m) else { + return Ok(None); + }; + let resolved = queries.resolved(file_id)?; + let Some((file_id, type_id)) = resolved.exports.lookup_type(n) else { + return Ok(None); + }; + Ok(Some(queries.intern_type(Type::Constructor(file_id, type_id)))) +} + +pub struct KnownTypesCore { + pub eq: Option<(FileId, TypeItemId)>, + pub eq1: Option<(FileId, TypeItemId)>, + pub ord: Option<(FileId, TypeItemId)>, + pub ord1: Option<(FileId, TypeItemId)>, + pub functor: Option<(FileId, TypeItemId)>, + pub bifunctor: Option<(FileId, TypeItemId)>, + pub contravariant: Option<(FileId, TypeItemId)>, + pub profunctor: Option<(FileId, TypeItemId)>, + pub foldable: Option<(FileId, TypeItemId)>, + pub bifoldable: Option<(FileId, TypeItemId)>, + pub traversable: Option<(FileId, TypeItemId)>, + pub bitraversable: Option<(FileId, TypeItemId)>, + pub newtype: Option<(FileId, TypeItemId)>, + pub generic: Option<(FileId, TypeItemId)>, +} + +impl KnownTypesCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let eq = fetch_known_class(queries, "Data.Eq", "Eq")?; + let eq1 = fetch_known_class(queries, "Data.Eq", "Eq1")?; + let ord = fetch_known_class(queries, "Data.Ord", "Ord")?; + let ord1 = fetch_known_class(queries, "Data.Ord", "Ord1")?; + let functor = fetch_known_class(queries, "Data.Functor", "Functor")?; + let bifunctor = fetch_known_class(queries, "Data.Bifunctor", "Bifunctor")?; + let contravariant = + fetch_known_class(queries, "Data.Functor.Contravariant", "Contravariant")?; + let profunctor = fetch_known_class(queries, "Data.Profunctor", "Profunctor")?; + let foldable = fetch_known_class(queries, "Data.Foldable", "Foldable")?; + let bifoldable = fetch_known_class(queries, "Data.Bifoldable", "Bifoldable")?; + let traversable = fetch_known_class(queries, "Data.Traversable", "Traversable")?; + let bitraversable = fetch_known_class(queries, "Data.Bitraversable", "Bitraversable")?; + let newtype = fetch_known_class(queries, "Data.Newtype", "Newtype")?; + let generic = fetch_known_class(queries, "Data.Generic.Rep", "Generic")?; + Ok(KnownTypesCore { + eq, + eq1, + ord, + ord1, + functor, + bifunctor, + contravariant, + profunctor, + foldable, + bifoldable, + traversable, + bitraversable, + newtype, + generic, + }) + } +} + +pub struct KnownReflectableCore { + pub is_symbol: Option<(FileId, TypeItemId)>, + pub reflectable: Option<(FileId, TypeItemId)>, + pub ordering: Option, +} + +impl KnownReflectableCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let is_symbol = fetch_known_class(queries, "Data.Symbol", "IsSymbol")?; + let reflectable = fetch_known_class(queries, "Data.Reflectable", "Reflectable")?; + let ordering = fetch_known_constructor(queries, "Data.Ordering", "Ordering")?; + Ok(KnownReflectableCore { is_symbol, reflectable, ordering }) + } +} + +pub struct KnownGeneric { + pub no_constructors: TypeId, + pub constructor: TypeId, + pub sum: TypeId, + pub product: TypeId, + pub no_arguments: TypeId, + pub argument: TypeId, +} + +impl KnownGeneric { + fn collect(queries: &impl ExternalQueries) -> QueryResult> { + let Some(no_constructors) = + fetch_known_constructor(queries, "Data.Generic.Rep", "NoConstructors")? + else { + return Ok(None); + }; + let Some(constructor) = + fetch_known_constructor(queries, "Data.Generic.Rep", "Constructor")? + else { + return Ok(None); + }; + let Some(sum) = fetch_known_constructor(queries, "Data.Generic.Rep", "Sum")? else { + return Ok(None); + }; + let Some(product) = fetch_known_constructor(queries, "Data.Generic.Rep", "Product")? else { + return Ok(None); + }; + let Some(no_arguments) = + fetch_known_constructor(queries, "Data.Generic.Rep", "NoArguments")? + else { + return Ok(None); + }; + let Some(argument) = fetch_known_constructor(queries, "Data.Generic.Rep", "Argument")? + else { + return Ok(None); + }; + Ok(Some(KnownGeneric { + no_constructors, + constructor, + sum, + product, + no_arguments, + argument, + })) + } +} + +pub struct KnownTermsCore { + pub otherwise: Option<(FileId, TermItemId)>, +} + +impl KnownTermsCore { + fn collect(queries: &impl ExternalQueries) -> QueryResult { + let otherwise = fetch_known_term(queries, "Data.Boolean", "otherwise")?; + Ok(KnownTermsCore { otherwise }) + } +} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index abfe95fc..b000d9ac 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1,3 +1,4 @@ +pub mod context; pub mod core; pub mod kind; From d97dfce2f4824d80a0c84d9ce5956ac749e0fc97 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 00:23:55 +0800 Subject: [PATCH 178/386] Implement CheckState and scaffolding for check_foreign_signature --- compiler-core/checking2/src/core.rs | 2 + .../checking2/src/core/generalise.rs | 38 ++++++++ compiler-core/checking2/src/kind.rs | 1 - compiler-core/checking2/src/lib.rs | 18 ++-- compiler-core/checking2/src/source.rs | 94 +++++++++++++++++++ compiler-core/checking2/src/source/terms.rs | 1 + compiler-core/checking2/src/source/types.rs | 23 +++++ compiler-core/checking2/src/state.rs | 39 ++++++++ justfile | 4 +- 9 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 compiler-core/checking2/src/core/generalise.rs delete mode 100644 compiler-core/checking2/src/kind.rs create mode 100644 compiler-core/checking2/src/source.rs create mode 100644 compiler-core/checking2/src/source/terms.rs create mode 100644 compiler-core/checking2/src/source/types.rs create mode 100644 compiler-core/checking2/src/state.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 96e9fb54..798871b3 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -1,5 +1,7 @@ //! Implements core type structures. +pub mod generalise; + use std::sync::Arc; use files::FileId; diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs new file mode 100644 index 00000000..5bac8cae --- /dev/null +++ b/compiler-core/checking2/src/core/generalise.rs @@ -0,0 +1,38 @@ +//! Implements generalisation algorithms for the core representation. +//! +//! Simply put, generalisation is an operation that takes some inferred +//! type full of unsolved [unification variables] and replaces them with +//! [universally quantified] [rigid type variables]. For example: +//! +//! ```purescript +//! id :: ?0 -> ?0 +//! ``` +//! +//! this will generalise into the following: +//! +//! ```purescript +//! id :: forall (t0 :: Type). t0 -> t0 +//! ``` +//! +//! [unification variables]: crate::core::Type::Unification +//! [universally quantified]: crate::core::Type::Forall +//! [rigid type variables]: crate::core::Type::Rigid + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::TypeId; +use crate::state::CheckState; + +/// Generalises a given type. See also module-level documentation. +pub fn generalise( + _state: &mut CheckState, + _context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + Ok(id) +} diff --git a/compiler-core/checking2/src/kind.rs b/compiler-core/checking2/src/kind.rs deleted file mode 100644 index bea8095c..00000000 --- a/compiler-core/checking2/src/kind.rs +++ /dev/null @@ -1 +0,0 @@ -//! Syntax-driven rules for kind checking. diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index b000d9ac..bfe980b3 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1,6 +1,7 @@ pub mod context; pub mod core; -pub mod kind; +pub mod source; +pub mod state; use std::sync::Arc; @@ -62,11 +63,16 @@ impl CheckedModule { pub fn check_module(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { let prim_id = queries.prim_id(); - if file_id == prim_id { - check_prim(queries, prim_id) - } else { - Ok(CheckedModule::default()) - } + if file_id == prim_id { check_prim(queries, prim_id) } else { check_source(queries, file_id) } +} + +fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { + let mut state = state::CheckState::new(file_id); + let context = context::CheckContext::new(queries, file_id)?; + + source::check_type_items(&mut state, &context)?; + + Ok(state.checked) } fn check_prim(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs new file mode 100644 index 00000000..e7589661 --- /dev/null +++ b/compiler-core/checking2/src/source.rs @@ -0,0 +1,94 @@ +//! Implements syntax-driven checking rules for source files. + +pub mod terms; +pub mod types; + +use building_types::QueryResult; +use indexing::TypeItemId; +use lowering::Scc; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::generalise; +use crate::state::CheckState; + +/// Checks all type items in topological order. +pub fn check_type_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.type_scc { + match scc { + Scc::Base(id) => { + check_type_signature(state, context, *id)?; + check_type_equation(state, context, *id)?; + } + Scc::Recursive(id) => { + check_type_signature(state, context, *id)?; + check_type_equation(state, context, *id)?; + } + Scc::Mutual(mutual) => { + for id in mutual { + check_type_signature(state, context, *id)?; + } + for &id in mutual { + check_type_equation(state, context, id)?; + } + } + } + } + Ok(()) +} + +fn check_type_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_type_item(item_id) else { + return Ok(()); + }; + + match item { + lowering::TypeItemIr::DataGroup { .. } => todo!(), + lowering::TypeItemIr::NewtypeGroup { .. } => todo!(), + lowering::TypeItemIr::SynonymGroup { .. } => todo!(), + lowering::TypeItemIr::ClassGroup { .. } => todo!(), + lowering::TypeItemIr::Foreign { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_foreign_signature(state, context, item_id, *signature)?; + } + lowering::TypeItemIr::Operator { .. } => todo!(), + } + + Ok(()) +} + +fn check_foreign_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + signature: lowering::TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; + let inferred_type = generalise::generalise(state, context, inferred_type)?; + state.checked.types.insert(item_id, inferred_type); + Ok(()) +} + +fn check_type_equation( + _state: &mut CheckState, + _context: &CheckContext, + _item_id: TypeItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + Ok(()) +} diff --git a/compiler-core/checking2/src/source/terms.rs b/compiler-core/checking2/src/source/terms.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/terms.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs new file mode 100644 index 00000000..17511d81 --- /dev/null +++ b/compiler-core/checking2/src/source/types.rs @@ -0,0 +1,23 @@ +//! Implements syntax-driven checking rules for types. + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::TypeId; +use crate::state::CheckState; + +/// Checks the kind of a syntax type against a core type. +/// +/// This function returns the core type and kind. +pub fn check_kind( + _state: &mut CheckState, + _context: &CheckContext, + _source_type: lowering::TypeId, + _expected_kind: TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + todo!() +} diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs new file mode 100644 index 00000000..3c29dcce --- /dev/null +++ b/compiler-core/checking2/src/state.rs @@ -0,0 +1,39 @@ +//! Implements the algorithm's core state structures. + +use files::FileId; + +use crate::CheckedModule; +use crate::core::Name; + +/// Yields globally unique [`Name`] values. +pub struct Names { + next: u32, + file: FileId, +} + +impl Names { + pub fn new(file: FileId) -> Names { + Names { next: 0, file } + } + + pub fn fresh(&mut self) -> Name { + let unique = self.next; + self.next += 1; + Name { file: self.file, unique } + } +} + +/// The core state structure threaded through the algorithm. +pub struct CheckState { + /// The output being built, populated by checking rules. + pub checked: CheckedModule, + + /// Produces fresh [`Name`] values for bound type variables. + pub names: Names, +} + +impl CheckState { + pub fn new(file_id: FileId) -> CheckState { + CheckState { checked: Default::default(), names: Names::new(file_id) } + } +} diff --git a/justfile b/justfile index 64388957..ab4bfd95 100644 --- a/justfile +++ b/justfile @@ -41,5 +41,5 @@ licenses: cargo bundle-licenses --prefer MIT -o ../THIRDPARTY.toml [doc("Format imports with module granularity")] -format-imports: - cargo +nightly fmt -- --config imports_granularity=Module +@format-imports *args="": + cargo +nightly fmt {{args}} -- --config imports_granularity=Module From fa41a491be2280202578b55ba7dc4ce57919f783 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 02:23:47 +0800 Subject: [PATCH 179/386] Implement safety utilities for use in checking rules --- compiler-core/checking2/src/lib.rs | 1 + compiler-core/checking2/src/safety.rs | 38 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 compiler-core/checking2/src/safety.rs diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index bfe980b3..76571d0f 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1,5 +1,6 @@ pub mod context; pub mod core; +pub mod safety; pub mod source; pub mod state; diff --git a/compiler-core/checking2/src/safety.rs b/compiler-core/checking2/src/safety.rs new file mode 100644 index 00000000..33d8b8d5 --- /dev/null +++ b/compiler-core/checking2/src/safety.rs @@ -0,0 +1,38 @@ +//! Safety utilities for implementing type checker rules. + +/// Fuel constant for bounded loops to prevent infinite looping. +pub const FUEL: u32 = 1_000_000; + +/// Executes a loop body with fuel, breaking when fuel runs out. +/// +/// Use this for loops that traverse type structures which could +/// theoretically be infinite due to bugs or malformed input. +/// +/// # Example +/// +/// ```ignore +/// let mut current_id = type_id; +/// safe_loop! { +/// current_id = normalise(current_id); +/// if let Type::Application(function, _) = context.queries.lookup_type(current_id) { +/// current_id = function; +/// } else { +/// break; +/// } +/// } +/// ``` +#[macro_export] +macro_rules! safe_loop { + ($($body:tt)*) => {{ + let mut fuel = 0u32; + loop { + if fuel >= $crate::safety::FUEL { + unreachable!("invariant violated: fuel exhausted"); + } + fuel += 1; + $($body)* + } + }}; +} + +pub use safe_loop; From 875977179a72078606bc31d9bc0bb211caa5388d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 02:00:26 +0800 Subject: [PATCH 180/386] Implement UnificationState for unification variable bookkeeping --- compiler-core/checking2/src/state.rs | 69 ++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 3c29dcce..5626807a 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -3,37 +3,86 @@ use files::FileId; use crate::CheckedModule; -use crate::core::Name; +use crate::core::{Depth, Name, TypeId}; -/// Yields globally unique [`Name`] values. +/// Manages [`Name`] values for [`CheckState`]. pub struct Names { - next: u32, + unique: u32, file: FileId, } impl Names { pub fn new(file: FileId) -> Names { - Names { next: 0, file } + Names { unique: 0, file } } pub fn fresh(&mut self) -> Name { - let unique = self.next; - self.next += 1; + let unique = self.unique; + self.unique += 1; Name { file: self.file, unique } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum UnificationState { + Unsolved, + Solved(TypeId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct UnificationEntry { + pub depth: Depth, + pub kind: TypeId, + pub state: UnificationState, +} + +/// Manages unification variables for [`CheckState`]. +#[derive(Debug, Default)] +pub struct Unifications { + entries: Vec, + unique: u32, +} + +impl Unifications { + pub fn fresh(&mut self, depth: Depth, kind: TypeId) -> u32 { + let unique = self.unique; + + self.unique += 1; + self.entries.push(UnificationEntry { depth, kind, state: UnificationState::Unsolved }); + + unique + } + + pub fn get(&self, index: u32) -> &UnificationEntry { + &self.entries[index as usize] + } + + pub fn get_mut(&mut self, index: u32) -> &mut UnificationEntry { + &mut self.entries[index as usize] + } + + pub fn solve(&mut self, index: u32, solution: TypeId) { + self.get_mut(index).state = UnificationState::Solved(solution); + } + + pub fn iter(&self) -> impl Iterator { + self.entries.iter() + } +} + /// The core state structure threaded through the algorithm. pub struct CheckState { - /// The output being built, populated by checking rules. pub checked: CheckedModule, - - /// Produces fresh [`Name`] values for bound type variables. pub names: Names, + pub unifications: Unifications, } impl CheckState { pub fn new(file_id: FileId) -> CheckState { - CheckState { checked: Default::default(), names: Names::new(file_id) } + CheckState { + checked: Default::default(), + names: Names::new(file_id), + unifications: Default::default(), + } } } From a0fc545a948c2b627c4b7933131a74307d1fd175 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 02:00:34 +0800 Subject: [PATCH 181/386] Implement core::normalise for unification variable pruning --- Cargo.lock | 1 + compiler-core/checking2/Cargo.toml | 1 + compiler-core/checking2/src/core.rs | 10 ++ compiler-core/checking2/src/core/normalise.rs | 117 ++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 compiler-core/checking2/src/core/normalise.rs diff --git a/Cargo.lock b/Cargo.lock index 21493b60..8a5b3726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,7 @@ dependencies = [ "files", "indexing", "interner", + "itertools 0.14.0", "lowering", "parsing", "resolving", diff --git a/compiler-core/checking2/Cargo.toml b/compiler-core/checking2/Cargo.toml index 2c9c98d1..71683527 100644 --- a/compiler-core/checking2/Cargo.toml +++ b/compiler-core/checking2/Cargo.toml @@ -8,6 +8,7 @@ building-types = { version = "0.1.0", path = "../building-types" } files = { version = "0.1.0", path = "../files" } indexing = { version = "0.1.0", path = "../indexing" } interner = { version = "0.1.0", path = "../interner" } +itertools = "0.14.0" lowering = { version = "0.1.0", path = "../lowering" } parsing = { version = "0.1.0", path = "../parsing" } resolving = { version = "0.1.0", path = "../resolving" } diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 798871b3..0f72176a 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -1,11 +1,13 @@ //! Implements core type structures. pub mod generalise; +pub mod normalise; use std::sync::Arc; use files::FileId; use indexing::TypeItemId; +use itertools::Itertools; use smol_str::SmolStr; /// A globally unique identity for a rigid type variable. @@ -39,6 +41,14 @@ pub struct RowType { pub tail: Option, } +impl RowType { + pub fn new(fields: impl IntoIterator, tail: Option) -> RowType { + let mut fields = fields.into_iter().collect_vec(); + fields.sort_by(|a, b| a.label.cmp(&b.label)); + RowType { fields: Arc::from(fields), tail } + } +} + /// A field in a row type. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RowField { diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs new file mode 100644 index 00000000..a79c5364 --- /dev/null +++ b/compiler-core/checking2/src/core/normalise.rs @@ -0,0 +1,117 @@ +//! Implements normalisation algorithms for the core representation. + +use building_types::QueryResult; +use itertools::Itertools; + +use crate::context::CheckContext; +use crate::core::{RowType, Type, TypeId}; +use crate::state::{CheckState, UnificationState}; +use crate::{ExternalQueries, safe_loop}; + +struct ReductionContext<'a, 'q, Q> +where + Q: ExternalQueries, +{ + state: &'a mut CheckState, + context: &'a CheckContext<'q, Q>, + compression: Vec, +} + +impl<'a, 'q, Q> ReductionContext<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn new(state: &'a mut CheckState, context: &'a CheckContext<'q, Q>) -> Self { + ReductionContext { state, context, compression: vec![] } + } + + fn reduce_once(&mut self, id: TypeId) -> Option { + let t = self.context.queries.lookup_type(id); + + if let Some(next) = self.rule_prune_unifications(&t) { + return Some(next); + } + if let Some(next) = self.rule_simplify_rows(&t) { + return Some(next); + } + + None + } + + fn rule_prune_unifications(&mut self, t: &Type) -> Option { + let Type::Unification(unification_id) = *t else { + return None; + }; + + let UnificationState::Solved(solution_id) = + self.state.unifications.get(unification_id).state + else { + return None; + }; + + self.compression.push(unification_id); + Some(solution_id) + } + + fn rule_simplify_rows(&self, t: &Type) -> Option { + let Type::Row(row_id) = *t else { + return None; + }; + + let row = self.context.queries.lookup_row_type(row_id); + + if row.fields.is_empty() { + return row.tail; + } + + let tail_id = row.tail?; + let tail_t = self.context.queries.lookup_type(tail_id); + + let Type::Row(inner_row_id) = tail_t else { + return None; + }; + + if inner_row_id == row_id { + return None; + } + + let inner = self.context.queries.lookup_row_type(inner_row_id); + + let merged_fields = { + let left = row.fields.iter().cloned(); + let right = inner.fields.iter().cloned(); + left.merge_by(right, |left, right| left.label <= right.label) + }; + + let merged_row = RowType::new(merged_fields, inner.tail); + let merged_row = self.context.queries.intern_row_type(merged_row); + let merged_row = self.context.queries.intern_type(Type::Row(merged_row)); + + Some(merged_row) + } +} + +pub fn normalise( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut reduction = ReductionContext::new(state, context); + + let id = safe_loop! { + if let Some(reduced_id) = reduction.reduce_once(id) { + id = reduced_id; + } else { + break id; + } + }; + + for unification_id in reduction.compression { + state.unifications.solve(unification_id, id); + } + + Ok(id) +} From 96944c9b14d669ecc3c7c1433623a1ad880838a4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 14:50:24 +0800 Subject: [PATCH 182/386] Implement ErrorCrumb, ErrorKind, CheckError --- compiler-core/checking2/src/error.rs | 149 +++++++++++++++++++++++++++ compiler-core/checking2/src/lib.rs | 3 + compiler-core/checking2/src/state.rs | 20 ++++ 3 files changed, 172 insertions(+) create mode 100644 compiler-core/checking2/src/error.rs diff --git a/compiler-core/checking2/src/error.rs b/compiler-core/checking2/src/error.rs new file mode 100644 index 00000000..5eef0b1f --- /dev/null +++ b/compiler-core/checking2/src/error.rs @@ -0,0 +1,149 @@ +//! Implements the errors emitted by the type checker. + +use std::sync::Arc; + +use smol_str::SmolStr; + +use crate::core::SmolStrId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCrumb { + TermDeclaration(indexing::TermItemId), + TypeDeclaration(indexing::TypeItemId), + ConstructorArgument(lowering::TypeId), + + InferringKind(lowering::TypeId), + CheckingKind(lowering::TypeId), + + InferringBinder(lowering::BinderId), + CheckingBinder(lowering::BinderId), + + InferringExpression(lowering::ExpressionId), + CheckingExpression(lowering::ExpressionId), + + InferringDoBind(lowering::DoStatementId), + InferringDoDiscard(lowering::DoStatementId), + CheckingDoLet(lowering::DoStatementId), + + InferringAdoMap(lowering::DoStatementId), + InferringAdoApply(lowering::DoStatementId), + CheckingAdoLet(lowering::DoStatementId), + + CheckingLetName(lowering::LetBindingNameGroupId), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ErrorKind { + AmbiguousConstraint { + constraint: SmolStrId, + }, + CannotDeriveClass { + class_file: files::FileId, + class_id: indexing::TypeItemId, + }, + CannotDeriveForType { + type_message: SmolStrId, + }, + ContravariantOccurrence { + type_message: SmolStrId, + }, + CovariantOccurrence { + type_message: SmolStrId, + }, + CannotUnify { + t1: SmolStrId, + t2: SmolStrId, + }, + DeriveInvalidArity { + class_file: files::FileId, + class_id: indexing::TypeItemId, + expected: usize, + actual: usize, + }, + DeriveMissingFunctor, + EmptyAdoBlock, + EmptyDoBlock, + InvalidFinalBind, + InvalidFinalLet, + InstanceHeadMismatch { + class_file: files::FileId, + class_item: indexing::TypeItemId, + expected: usize, + actual: usize, + }, + InstanceHeadLabeledRow { + class_file: files::FileId, + class_item: indexing::TypeItemId, + position: usize, + type_message: SmolStrId, + }, + InstanceMemberTypeMismatch { + expected: SmolStrId, + actual: SmolStrId, + }, + InvalidTypeApplication { + function_type: SmolStrId, + function_kind: SmolStrId, + argument_type: SmolStrId, + }, + InvalidTypeOperator { + kind_message: SmolStrId, + }, + ExpectedNewtype { + type_message: SmolStrId, + }, + InvalidNewtypeDeriveSkolemArguments, + NoInstanceFound { + constraint: SmolStrId, + }, + PartialSynonymApplication { + id: lowering::TypeId, + }, + RecursiveSynonymExpansion { + file_id: files::FileId, + item_id: indexing::TypeItemId, + }, + TooManyBinders { + signature: lowering::TypeId, + expected: u32, + actual: u32, + }, + TypeSignatureVariableMismatch { + id: lowering::TypeId, + expected: u32, + actual: u32, + }, + InvalidRoleDeclaration { + index: usize, + declared: crate::core::Role, + inferred: crate::core::Role, + }, + CoercibleConstructorNotInScope { + file_id: files::FileId, + item_id: indexing::TypeItemId, + }, + CustomWarning { + message_id: SmolStrId, + }, + RedundantPatterns { + patterns: Arc<[SmolStr]>, + }, + MissingPatterns { + patterns: Arc<[SmolStrId]>, + }, + CustomFailure { + message_id: SmolStrId, + }, + PropertyIsMissing { + labels: Arc<[SmolStr]>, + }, + AdditionalProperty { + labels: Arc<[SmolStr]>, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct CheckError { + pub kind: ErrorKind, + pub crumbs: Arc<[ErrorCrumb]>, +} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 76571d0f..54399731 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1,5 +1,6 @@ pub mod context; pub mod core; +pub mod error; pub mod safety; pub mod source; pub mod state; @@ -16,6 +17,7 @@ use smol_str::SmolStr; use crate::core::{ ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, }; +use crate::error::CheckError; pub trait ExternalQueries: QueryProxy< @@ -50,6 +52,7 @@ pub trait ExternalQueries: pub struct CheckedModule { pub types: FxHashMap, pub roles: FxHashMap>, + pub errors: Vec, } impl CheckedModule { diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 5626807a..337c6d53 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -4,6 +4,7 @@ use files::FileId; use crate::CheckedModule; use crate::core::{Depth, Name, TypeId}; +use crate::error::{CheckError, ErrorCrumb, ErrorKind}; /// Manages [`Name`] values for [`CheckState`]. pub struct Names { @@ -73,8 +74,11 @@ impl Unifications { /// The core state structure threaded through the algorithm. pub struct CheckState { pub checked: CheckedModule, + pub names: Names, pub unifications: Unifications, + + pub crumbs: Vec, } impl CheckState { @@ -83,6 +87,22 @@ impl CheckState { checked: Default::default(), names: Names::new(file_id), unifications: Default::default(), + crumbs: Default::default(), } } + + pub fn with_error_crumb(&mut self, crumb: ErrorCrumb, f: F) -> T + where + F: FnOnce(&mut CheckState) -> T, + { + self.crumbs.push(crumb); + let result = f(self); + self.crumbs.pop(); + result + } + + pub fn insert_error(&mut self, kind: ErrorKind) { + let crumbs = self.crumbs.iter().copied().collect(); + self.checked.errors.push(CheckError { kind, crumbs }); + } } From 65ada2ff814e79b9f6e7edc57d9e7d757aa20e61 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 15:59:10 +0800 Subject: [PATCH 183/386] Implement initial utilities for constructing types --- compiler-core/checking2/src/context.rs | 121 ++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index 798b9f6f..fd747f6e 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -13,7 +13,9 @@ use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; use crate::ExternalQueries; -use crate::core::{Type, TypeId}; +use crate::core::{ + ForallBinder, ForallBinderId, Name, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, +}; /// The read-only environment threaded through the type checking algorithm. /// @@ -113,7 +115,122 @@ where } } -struct PrimLookup<'r, 'q, Q: ExternalQueries> { +impl<'q, Q> CheckContext<'q, Q> +where + Q: ExternalQueries, +{ + /// Interns a [`Type::Application`] node. + pub fn intern_application(&self, function: TypeId, argument: TypeId) -> TypeId { + self.queries.intern_type(Type::Application(function, argument)) + } + + /// Interns a [`Type::KindApplication`] node. + pub fn intern_kind_application(&self, function: TypeId, argument: TypeId) -> TypeId { + self.queries.intern_type(Type::KindApplication(function, argument)) + } + + /// Interns a [`Type::OperatorApplication`] node. + pub fn intern_operator_application( + &self, + file_id: FileId, + type_id: TypeItemId, + left: TypeId, + right: TypeId, + ) -> TypeId { + self.queries.intern_type(Type::OperatorApplication(file_id, type_id, left, right)) + } + + /// Interns a [`Type::SynonymApplication`] node. + pub fn intern_synonym_application(&self, synonym_id: SynonymId) -> TypeId { + self.queries.intern_type(Type::SynonymApplication(synonym_id)) + } + + /// Interns a [`Type::Forall`] node. + pub fn intern_forall(&self, binder_id: ForallBinderId, inner: TypeId) -> TypeId { + self.queries.intern_type(Type::Forall(binder_id, inner)) + } + + /// Interns a [`Type::Constrained`] node. + pub fn intern_constrained(&self, constraint: TypeId, inner: TypeId) -> TypeId { + self.queries.intern_type(Type::Constrained(constraint, inner)) + } + + /// Interns a [`Type::Function`] node. + pub fn intern_function(&self, argument: TypeId, result: TypeId) -> TypeId { + self.queries.intern_type(Type::Function(argument, result)) + } + + /// Interns a [`Type::Kinded`] node. + pub fn intern_kinded(&self, inner: TypeId, kind: TypeId) -> TypeId { + self.queries.intern_type(Type::Kinded(inner, kind)) + } + + /// Interns a [`Type::Row`] node. + pub fn intern_row(&self, row_id: RowTypeId) -> TypeId { + self.queries.intern_type(Type::Row(row_id)) + } + + /// Interns a [`Type::Rigid`] node. + pub fn intern_rigid(&self, name: Name, kind: TypeId) -> TypeId { + self.queries.intern_type(Type::Rigid(name, kind)) + } + + /// Interns a [`Type::Application`]-based function. + /// + /// The types `Function a b` and `a -> b` are equivalent, represented by + /// [`Type::Application`] and [`Type::Function`] respectively. Normalising + /// into the application-based form is generally more useful, such as in + /// the following example: + /// + /// ```text + /// unify(?function_a b, a -> b) + /// unify(?function_a b, Function a b) = [ ?function_a := Function a ] + /// ``` + pub fn intern_function_application(&self, argument: TypeId, result: TypeId) -> TypeId { + let function_argument = self.intern_application(self.prim.function, argument); + self.intern_application(function_argument, result) + } + + /// Looks up the [`Type`] for the given [`TypeId`]. + pub fn lookup_type(&self, id: TypeId) -> Type { + self.queries.lookup_type(id) + } + + /// Looks up the [`ForallBinder`] for the given [`ForallBinderId`]. + pub fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder { + self.queries.lookup_forall_binder(id) + } + + /// Looks up the [`RowType`] for the given [`RowTypeId`]. + pub fn lookup_row_type(&self, id: RowTypeId) -> RowType { + self.queries.lookup_row_type(id) + } + + /// Looks up the [`Synonym`] for the given [`SynonymId`]. + pub fn lookup_synonym(&self, id: SynonymId) -> Synonym { + self.queries.lookup_synonym(id) + } + + /// Interns a [`ForallBinder`], returning its [`ForallBinderId`]. + pub fn intern_forall_binder(&self, binder: ForallBinder) -> ForallBinderId { + self.queries.intern_forall_binder(binder) + } + + /// Interns a [`RowType`], returning its [`RowTypeId`]. + pub fn intern_row_type(&self, row: RowType) -> RowTypeId { + self.queries.intern_row_type(row) + } + + /// Interns a [`Synonym`], returning its [`SynonymId`]. + pub fn intern_synonym(&self, synonym: Synonym) -> SynonymId { + self.queries.intern_synonym(synonym) + } +} + +struct PrimLookup<'r, 'q, Q> +where + Q: ExternalQueries, +{ resolved: &'r ResolvedModule, queries: &'q Q, module_name: &'static str, From e6bfa4ab391ace299326db68658f3304a94a1493 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 16:55:27 +0800 Subject: [PATCH 184/386] Add depth field to Type::Rigid --- compiler-core/checking2/src/context.rs | 6 +++--- compiler-core/checking2/src/core.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index fd747f6e..2174b0cd 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -14,7 +14,7 @@ use sugar::{Bracketed, Sectioned}; use crate::ExternalQueries; use crate::core::{ - ForallBinder, ForallBinderId, Name, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, + Depth, ForallBinder, ForallBinderId, Name, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, }; /// The read-only environment threaded through the type checking algorithm. @@ -171,8 +171,8 @@ where } /// Interns a [`Type::Rigid`] node. - pub fn intern_rigid(&self, name: Name, kind: TypeId) -> TypeId { - self.queries.intern_type(Type::Rigid(name, kind)) + pub fn intern_rigid(&self, name: Name, depth: Depth, kind: TypeId) -> TypeId { + self.queries.intern_type(Type::Rigid(name, depth, kind)) } /// Interns a [`Type::Application`]-based function. diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 0f72176a..8f086fa9 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -112,7 +112,7 @@ pub enum Type { Row(RowTypeId), /// A bound skolem variable that can only unify with itself. - Rigid(Name, TypeId), + Rigid(Name, Depth, TypeId), /// A unification variable that can be solved to another [`Type`]. Unification(u32), From 9e8a75c4d2d5b468583357a620f39aeaa0ba6b6a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 16:57:29 +0800 Subject: [PATCH 185/386] Add fold and walk abstractions for core --- compiler-core/checking2/src/core.rs | 2 + compiler-core/checking2/src/core/fold.rs | 115 +++++++++++++++++++++++ compiler-core/checking2/src/core/walk.rs | 96 +++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 compiler-core/checking2/src/core/fold.rs create mode 100644 compiler-core/checking2/src/core/walk.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 8f086fa9..e74c4945 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -1,7 +1,9 @@ //! Implements core type structures. +pub mod fold; pub mod generalise; pub mod normalise; +pub mod walk; use std::sync::Arc; diff --git a/compiler-core/checking2/src/core/fold.rs b/compiler-core/checking2/src/core/fold.rs new file mode 100644 index 00000000..38be8e12 --- /dev/null +++ b/compiler-core/checking2/src/core/fold.rs @@ -0,0 +1,115 @@ +//! Implements type folding for the core representation. + +use std::sync::Arc; + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::normalise::normalise; +use crate::core::{ForallBinder, Type, TypeId}; +use crate::state::CheckState; + +pub enum FoldAction { + Replace(TypeId), + Continue, +} + +pub trait TypeFold { + fn transform( + &mut self, + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + t: &Type, + ) -> QueryResult; + + fn transform_binder(&mut self, _binder: &mut ForallBinder) {} +} + +pub fn fold_type( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + folder: &mut F, +) -> QueryResult +where + Q: ExternalQueries, + F: TypeFold, +{ + let id = normalise(state, context, id)?; + let t = context.lookup_type(id); + + if let FoldAction::Replace(id) = folder.transform(state, context, id, &t)? { + return Ok(id); + } + + let result = match t { + Type::Application(function, argument) => { + let function = fold_type(state, context, function, folder)?; + let argument = fold_type(state, context, argument, folder)?; + context.intern_application(function, argument) + } + Type::KindApplication(function, argument) => { + let function = fold_type(state, context, function, folder)?; + let argument = fold_type(state, context, argument, folder)?; + context.intern_kind_application(function, argument) + } + Type::OperatorApplication(file_id, type_id, left, right) => { + let left = fold_type(state, context, left, folder)?; + let right = fold_type(state, context, right, folder)?; + context.intern_operator_application(file_id, type_id, left, right) + } + Type::SynonymApplication(synonym_id) => { + let mut synonym = context.lookup_synonym(synonym_id); + synonym.arguments = synonym + .arguments + .iter() + .map(|&argument| fold_type(state, context, argument, folder)) + .collect::>()?; + let synonym_id = context.intern_synonym(synonym); + context.intern_synonym_application(synonym_id) + } + Type::Forall(binder_id, inner) => { + let mut binder = context.lookup_forall_binder(binder_id); + folder.transform_binder(&mut binder); + binder.kind = fold_type(state, context, binder.kind, folder)?; + let inner = fold_type(state, context, inner, folder)?; + let binder_id = context.intern_forall_binder(binder); + context.intern_forall(binder_id, inner) + } + Type::Constrained(constraint, inner) => { + let constraint = fold_type(state, context, constraint, folder)?; + let inner = fold_type(state, context, inner, folder)?; + context.intern_constrained(constraint, inner) + } + Type::Function(argument, result) => { + let argument = fold_type(state, context, argument, folder)?; + let result = fold_type(state, context, result, folder)?; + context.intern_function(argument, result) + } + Type::Kinded(inner, kind) => { + let inner = fold_type(state, context, inner, folder)?; + let kind = fold_type(state, context, kind, folder)?; + context.intern_kinded(inner, kind) + } + Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => id, + Type::Integer(_) | Type::String(_, _) => id, + Type::Row(row_id) => { + let mut row = context.lookup_row_type(row_id); + for field in Arc::make_mut(&mut row.fields).iter_mut() { + field.id = fold_type(state, context, field.id, folder)?; + } + row.tail = row.tail.map(|tail| fold_type(state, context, tail, folder)).transpose()?; + let row_id = context.intern_row_type(row); + context.intern_row(row_id) + } + Type::Rigid(name, depth, kind) => { + let kind = fold_type(state, context, kind, folder)?; + context.intern_rigid(name, depth, kind) + } + Type::Unification(_) | Type::Free(_) | Type::Unknown(_) => id, + }; + + Ok(result) +} diff --git a/compiler-core/checking2/src/core/walk.rs b/compiler-core/checking2/src/core/walk.rs new file mode 100644 index 00000000..65765d27 --- /dev/null +++ b/compiler-core/checking2/src/core/walk.rs @@ -0,0 +1,96 @@ +//! Implements type walking for the core representation. + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::normalise::normalise; +use crate::core::{ForallBinder, Type, TypeId}; +use crate::state::CheckState; + +pub enum WalkAction { + Stop, + Continue, +} + +pub trait TypeWalker { + fn visit( + &mut self, + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + t: &Type, + ) -> QueryResult; + + fn visit_binder(&mut self, _binder: &ForallBinder) {} +} + +pub fn walk_type( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + walker: &mut W, +) -> QueryResult<()> +where + Q: ExternalQueries, + W: TypeWalker, +{ + let id = normalise(state, context, id)?; + let t = context.queries.lookup_type(id); + + if let WalkAction::Stop = walker.visit(state, context, id, &t)? { + return Ok(()); + } + + match t { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + walk_type(state, context, function, walker)?; + walk_type(state, context, argument, walker)?; + } + Type::OperatorApplication(_, _, left, right) => { + walk_type(state, context, left, walker)?; + walk_type(state, context, right, walker)?; + } + Type::SynonymApplication(synonym_id) => { + let synonym = context.queries.lookup_synonym(synonym_id); + for &argument in synonym.arguments.iter() { + walk_type(state, context, argument, walker)?; + } + } + Type::Forall(binder_id, inner) => { + let binder = context.queries.lookup_forall_binder(binder_id); + walker.visit_binder(&binder); + walk_type(state, context, binder.kind, walker)?; + walk_type(state, context, inner, walker)?; + } + Type::Constrained(constraint, inner) => { + walk_type(state, context, constraint, walker)?; + walk_type(state, context, inner, walker)?; + } + Type::Function(argument, result) => { + walk_type(state, context, argument, walker)?; + walk_type(state, context, result, walker)?; + } + Type::Kinded(inner, kind) => { + walk_type(state, context, inner, walker)?; + walk_type(state, context, kind, walker)?; + } + Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => {} + Type::Integer(_) | Type::String(_, _) => {} + Type::Row(row_id) => { + let row = context.queries.lookup_row_type(row_id); + for field in row.fields.iter() { + walk_type(state, context, field.id, walker)?; + } + if let Some(tail) = row.tail { + walk_type(state, context, tail, walker)?; + } + } + Type::Rigid(_, _, kind) => { + walk_type(state, context, kind, walker)?; + } + Type::Unification(_) | Type::Free(_) | Type::Unknown(_) => {} + } + + Ok(()) +} From 112d944bc89c3d7d2383ee6d28c5dc7c3fd240b6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 17:23:18 +0800 Subject: [PATCH 186/386] Implement core::substitute and SubstituteName --- compiler-core/checking2/src/core.rs | 1 + .../checking2/src/core/substitute.rs | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 compiler-core/checking2/src/core/substitute.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index e74c4945..834b94a6 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -3,6 +3,7 @@ pub mod fold; pub mod generalise; pub mod normalise; +pub mod substitute; pub mod walk; use std::sync::Arc; diff --git a/compiler-core/checking2/src/core/substitute.rs b/compiler-core/checking2/src/core/substitute.rs new file mode 100644 index 00000000..02e1a735 --- /dev/null +++ b/compiler-core/checking2/src/core/substitute.rs @@ -0,0 +1,71 @@ +//! Implements name-based type substitution for the core representation. + +use rustc_hash::FxHashMap; + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::fold::{FoldAction, TypeFold, fold_type}; +use crate::core::{Name, Type, TypeId}; +use crate::state::CheckState; + +pub type NameToType = FxHashMap; + +/// Substitutes rigid type variables identified by [`Name`] with replacement types. +/// +/// Since names are globally unique, no scope tracking is needed. This subsumes +/// both single-name substitution (for instantiation) and multi-name substitution +/// (for specialising class superclasses with instance arguments). +pub struct SubstituteName { + bindings: NameToType, +} + +impl SubstituteName { + pub fn one( + state: &mut CheckState, + context: &CheckContext, + name: Name, + replacement: TypeId, + in_type: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + let bindings = NameToType::from_iter([(name, replacement)]); + fold_type(state, context, in_type, &mut SubstituteName { bindings }) + } + + pub fn many( + state: &mut CheckState, + context: &CheckContext, + bindings: NameToType, + in_type: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + fold_type(state, context, in_type, &mut SubstituteName { bindings }) + } +} + +impl TypeFold for SubstituteName { + fn transform( + &mut self, + _state: &mut CheckState, + _context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Rigid(name, _, _) = t + && let Some(id) = self.bindings.get(name) + { + Ok(FoldAction::Replace(*id)) + } else { + Ok(FoldAction::Continue) + } + } +} From e2e8b4f87073597223266b06e26f177f7cd2c32f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 19:13:59 +0800 Subject: [PATCH 187/386] Add depth tracking and with_depth to CheckState --- compiler-core/checking2/src/core.rs | 6 ++++++ compiler-core/checking2/src/state.rs | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 834b94a6..3d3381e1 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -24,6 +24,12 @@ pub struct Name { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Depth(pub u32); +impl Depth { + pub fn increment(self) -> Depth { + Depth(self.0 + 1) + } +} + /// Carries information about a type variable under a forall. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ForallBinder { diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 337c6d53..80cac24f 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -1,5 +1,7 @@ //! Implements the algorithm's core state structures. +use std::mem; + use files::FileId; use crate::CheckedModule; @@ -77,6 +79,7 @@ pub struct CheckState { pub names: Names, pub unifications: Unifications, + pub depth: Depth, pub crumbs: Vec, } @@ -87,10 +90,21 @@ impl CheckState { checked: Default::default(), names: Names::new(file_id), unifications: Default::default(), + depth: Depth(0), crumbs: Default::default(), } } + pub fn with_depth(&mut self, f: impl FnOnce(&mut CheckState) -> T) -> T { + let depth = self.depth.increment(); + + let previous = mem::replace(&mut self.depth, depth); + let result = f(self); + self.depth = previous; + + result + } + pub fn with_error_crumb(&mut self, crumb: ErrorCrumb, f: F) -> T where F: FnOnce(&mut CheckState) -> T, From f85899dfabb456fd83246956bf14163bd36f6e12 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 19:10:14 +0800 Subject: [PATCH 188/386] Add fresh_unification and fresh_rigid helpers to CheckState --- compiler-core/checking2/src/state.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 80cac24f..1dd36e24 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -4,9 +4,9 @@ use std::mem; use files::FileId; -use crate::CheckedModule; -use crate::core::{Depth, Name, TypeId}; +use crate::core::{Depth, Name, Type, TypeId}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; +use crate::{CheckedModule, ExternalQueries}; /// Manages [`Name`] values for [`CheckState`]. pub struct Names { @@ -115,6 +115,16 @@ impl CheckState { result } + pub fn fresh_unification(&mut self, queries: &impl ExternalQueries, kind: TypeId) -> TypeId { + let unification = self.unifications.fresh(self.depth, kind); + queries.intern_type(Type::Unification(unification)) + } + + pub fn fresh_rigid(&mut self, queries: &impl ExternalQueries, kind: TypeId) -> TypeId { + let name = self.names.fresh(); + queries.intern_type(Type::Rigid(name, self.depth, kind)) + } + pub fn insert_error(&mut self, kind: ErrorKind) { let crumbs = self.crumbs.iter().copied().collect(); self.checked.errors.push(CheckError { kind, crumbs }); From 4fe0bcdb72404469ee799f21db7b8912cf78c8c9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 02:01:25 +0800 Subject: [PATCH 189/386] Implement core::unification and initial rules --- compiler-core/checking2/src/core.rs | 1 + .../checking2/src/core/unification.rs | 290 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 compiler-core/checking2/src/core/unification.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 3d3381e1..dd9a5dce 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -4,6 +4,7 @@ pub mod fold; pub mod generalise; pub mod normalise; pub mod substitute; +pub mod unification; pub mod walk; use std::sync::Arc; diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs new file mode 100644 index 00000000..84bac9d1 --- /dev/null +++ b/compiler-core/checking2/src/core/unification.rs @@ -0,0 +1,290 @@ +//! Implements the subtyping and unification algorithms. + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{RowTypeId, Type, TypeId, normalise}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +/// Determines if constraints are elaborated during [`subtype`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ElaborationMode { + Yes, + No, +} + +pub fn subtype( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + subtype_with_mode(state, context, ElaborationMode::Yes, t1, t2) +} + +pub fn subtype_with_mode( + state: &mut CheckState, + context: &CheckContext, + mode: ElaborationMode, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(true); + } + + let t1_core = context.queries.lookup_type(t1); + let t2_core = context.queries.lookup_type(t2); + + match (t1_core, t2_core) { + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { + Ok(subtype_with_mode(state, context, ElaborationMode::No, t2_argument, t1_argument)? + && subtype_with_mode(state, context, ElaborationMode::No, t1_result, t2_result)?) + } + + (Type::Application(_, _), Type::Function(t2_argument, t2_result)) => { + let t2 = context.intern_function_application(t2_argument, t2_result); + subtype_with_mode(state, context, mode, t1, t2) + } + + (Type::Function(t1_argument, t1_result), Type::Application(_, _)) => { + let t1 = context.intern_function_application(t1_argument, t1_result); + subtype_with_mode(state, context, mode, t1, t2) + } + + (_, Type::Forall(binder_id, inner)) => { + let binder = context.lookup_forall_binder(binder_id); + let skolem = state.fresh_rigid(context.queries, binder.kind); + + let inner = SubstituteName::one(state, context, binder.name, skolem, inner)?; + state.with_depth(|state| subtype_with_mode(state, context, mode, t1, inner)) + } + + (Type::Forall(binder_id, inner), _) => { + let binder = context.lookup_forall_binder(binder_id); + let unification = state.fresh_unification(context.queries, binder.kind); + + let inner = SubstituteName::one(state, context, binder.name, unification, inner)?; + subtype_with_mode(state, context, mode, inner, t2) + } + + (Type::Constrained(constraint, inner), _) if mode == ElaborationMode::Yes => { + // TODO: implication constraints + // + // state.push_wanted(constraint); + // subtype_with_mode(state, context, inner, t2, mode) + let _ = (constraint, inner); + Ok(false) + } + + ( + Type::Application(t1_function, t1_argument), + Type::Application(t2_function, t2_argument), + ) if t1_function == context.prim.record && t2_function == context.prim.record => { + let t1_argument = normalise::normalise(state, context, t1_argument)?; + let t2_argument = normalise::normalise(state, context, t2_argument)?; + + let t1_argument_core = context.queries.lookup_type(t1_argument); + let t2_argument_core = context.queries.lookup_type(t2_argument); + + if let (Type::Row(t1_row_id), Type::Row(t2_row_id)) = + (t1_argument_core, t2_argument_core) + { + subtype_rows(state, context, mode, t1_row_id, t2_row_id) + } else { + unify(state, context, t1, t2) + } + } + + (_, _) => unify(state, context, t1, t2), + } +} + +pub fn unify( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(true); + } + + let t1_core = context.queries.lookup_type(t1); + let t2_core = context.queries.lookup_type(t2); + + let unifies = match (t1_core, t2_core) { + // TODO: document impredicativity + (Type::Unification(id), _) => return solve(state, context, id, t2), + (_, Type::Unification(id)) => return solve(state, context, id, t1), + + ( + Type::Application(t1_function, t1_argument), + Type::Application(t2_function, t2_argument), + ) => { + unify(state, context, t1_function, t2_function)? + && unify(state, context, t1_argument, t2_argument)? + } + + ( + Type::KindApplication(t1_function, t1_argument), + Type::KindApplication(t2_function, t2_argument), + ) => { + unify(state, context, t1_function, t2_function)? + && unify(state, context, t1_argument, t2_argument)? + } + + ( + Type::OperatorApplication(t1_file, t1_type, t1_left, t1_right), + Type::OperatorApplication(t2_file, t2_type, t2_left, t2_right), + ) if t1_file == t2_file && t1_type == t2_type => { + unify(state, context, t1_left, t2_left)? && unify(state, context, t1_right, t2_right)? + } + + (Type::Forall(t1_binder_id, t1_inner), Type::Forall(t2_binder_id, t2_inner)) => { + let t1_binder = context.lookup_forall_binder(t1_binder_id); + let t2_binder = context.lookup_forall_binder(t2_binder_id); + + unify(state, context, t1_binder.kind, t2_binder.kind)?; + + let skolem = state.fresh_rigid(context.queries, t1_binder.kind); + + let t1_inner = SubstituteName::one(state, context, t1_binder.name, skolem, t1_inner)?; + let t2_inner = SubstituteName::one(state, context, t2_binder.name, skolem, t2_inner)?; + + state.with_depth(|state| unify(state, context, t1_inner, t2_inner))? + } + + (Type::Forall(binder_id, inner), _) => { + let binder = context.lookup_forall_binder(binder_id); + let skolem = state.fresh_rigid(context.queries, binder.kind); + let inner = SubstituteName::one(state, context, binder.name, skolem, inner)?; + state.with_depth(|state| unify(state, context, inner, t2))? + } + + (_, Type::Forall(binder_id, inner)) => { + let binder = context.lookup_forall_binder(binder_id); + let skolem = state.fresh_rigid(context.queries, binder.kind); + let inner = SubstituteName::one(state, context, binder.name, skolem, inner)?; + state.with_depth(|state| unify(state, context, t1, inner))? + } + + ( + Type::Constrained(t1_constraint, t1_inner), + Type::Constrained(t2_constraint, t2_inner), + ) => { + unify(state, context, t1_constraint, t2_constraint)? + && unify(state, context, t1_inner, t2_inner)? + } + + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { + unify(state, context, t1_argument, t2_argument)? + && unify(state, context, t1_result, t2_result)? + } + + (Type::Kinded(t1_inner, t1_kind), Type::Kinded(t2_inner, t2_kind)) => { + unify(state, context, t1_inner, t2_inner)? && unify(state, context, t1_kind, t2_kind)? + } + + (Type::Constructor(t1_file, t1_item), Type::Constructor(t2_file, t2_item)) + if t1_file == t2_file && t1_item == t2_item => + { + true + } + + ( + Type::OperatorConstructor(t1_file, t1_item), + Type::OperatorConstructor(t2_file, t2_item), + ) if t1_file == t2_file && t1_item == t2_item => true, + + (Type::Integer(t1_value), Type::Integer(t2_value)) if t1_value == t2_value => true, + + (Type::String(t1_kind, t1_value), Type::String(t2_kind, t2_value)) + if t1_kind == t2_kind && t1_value == t2_value => + { + true + } + + (Type::Row(t1_row_id), Type::Row(t2_row_id)) => { + unify_rows(state, context, t1_row_id, t2_row_id)? + } + + (Type::Rigid(t1_name, _, t1_kind), Type::Rigid(t2_name, _, t2_kind)) + if t1_name == t2_name => + { + unify(state, context, t1_kind, t2_kind)? + } + + _ => false, + }; + + if !unifies { + // TODO: pretty-print types for error messages + let t1 = context.queries.intern_smol_str("?".into()); + let t2 = context.queries.intern_smol_str("?".into()); + state.insert_error(ErrorKind::CannotUnify { t1, t2 }); + } + + Ok(unifies) +} + +/// Solves a unification variable to a given type. +fn solve( + state: &mut CheckState, + context: &CheckContext, + unification_id: u32, + solution: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let _ = (state, context, unification_id, solution); + Ok(false) +} + +fn unify_rows( + state: &mut CheckState, + context: &CheckContext, + t1: RowTypeId, + t2: RowTypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO: implement row unification + let _ = (state, context, t1, t2); + Ok(false) +} + +fn subtype_rows( + state: &mut CheckState, + context: &CheckContext, + mode: ElaborationMode, + t1: RowTypeId, + t2: RowTypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO: implement row subtyping + let _ = (state, context, mode, t1, t2); + Ok(false) +} From 1c54bbd85c803ca071d3442d7d433a88a497df33 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 18 Feb 2026 19:30:34 +0800 Subject: [PATCH 190/386] Implement unification variable solving and promotion --- .../checking2/src/core/unification.rs | 209 +++++++++++++++++- 1 file changed, 204 insertions(+), 5 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 84bac9d1..e398e6f7 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -5,9 +5,9 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{RowTypeId, Type, TypeId, normalise}; +use crate::core::{Name, RowTypeId, Type, TypeId, normalise}; use crate::error::ErrorKind; -use crate::state::CheckState; +use crate::state::{CheckState, UnificationEntry}; /// Determines if constraints are elaborated during [`subtype`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -247,7 +247,7 @@ where } /// Solves a unification variable to a given type. -fn solve( +pub fn solve( state: &mut CheckState, context: &CheckContext, unification_id: u32, @@ -256,8 +256,207 @@ fn solve( where Q: ExternalQueries, { - let _ = (state, context, unification_id, solution); - Ok(false) + let solution = normalise::normalise(state, context, solution)?; + + match promote_type(state, context, unification_id, solution)? { + PromoteResult::Ok => {} + PromoteResult::OccursCheck => { + // TODO: pretty-print types for error messages + let t1 = context.queries.intern_smol_str("?".into()); + let t2 = context.queries.intern_smol_str("?".into()); + state.insert_error(ErrorKind::CannotUnify { t1, t2 }); + return Ok(false); + } + PromoteResult::SkolemEscape => { + // TODO: pretty-print types for error messages + let t1 = context.queries.intern_smol_str("?".into()); + let t2 = context.queries.intern_smol_str("?".into()); + state.insert_error(ErrorKind::CannotUnify { t1, t2 }); + return Ok(false); + } + } + + let unification_kind = state.unifications.get(unification_id).kind; + // TODO: unify kinds once kind elaboration is available + let _ = unification_kind; + + state.unifications.solve(unification_id, solution); + Ok(true) +} + +enum PromoteResult { + Ok, + OccursCheck, + SkolemEscape, +} + +impl PromoteResult { + fn and_then( + self, + f: impl FnOnce() -> QueryResult, + ) -> QueryResult { + match self { + PromoteResult::Ok => f(), + result => Ok(result), + } + } +} + +/// Checks that solving a unification variable is safe. +/// +/// This function has several responsibilities: +/// - the [occurs check], where a solution that contains the unification +/// variable being solved is rejected. This avoids infinite types; +/// - the [skolem escape check], where a solution that contains a rigid +/// type variable deeper than the unification variable is rejected; +/// - lastly, promotion which solves deeper unification variables into +/// fresh unification variables as shallow as the one being solved. +/// +/// Since PureScript is an impredicative language i.e. it allows unification +/// variables to be solved to [`Type::Forall`], we keep track of the names +/// bound in the context specifically to allow `?t := forall a. a -> a` to +/// skip the skolem escape check completely. +/// +/// [occurs check]: PromoteResult::OccursCheck +/// [skolem escape check]: PromoteResult::SkolemEscape +fn promote_type( + state: &mut CheckState, + context: &CheckContext, + unification_id: u32, + solution: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + struct PromotionState { + unification_id: u32, + unification_depth: crate::core::Depth, + bound_names: Vec, + } + + fn check( + promote: &mut PromotionState, + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + let id = normalise::normalise(state, context, id)?; + let t = context.queries.lookup_type(id); + + match t { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + check(promote, state, context, function)? + .and_then(|| check(promote, state, context, argument)) + } + + Type::OperatorApplication(_, _, left, right) => check(promote, state, context, left)? + .and_then(|| check(promote, state, context, right)), + + Type::SynonymApplication(synonym_id) => { + let synonym = context.lookup_synonym(synonym_id); + for &argument in synonym.arguments.iter() { + let result = check(promote, state, context, argument)?; + if !matches!(result, PromoteResult::Ok) { + return Ok(result); + } + } + Ok(PromoteResult::Ok) + } + + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + + let on_kind = check(promote, state, context, binder.kind)?; + if !matches!(on_kind, PromoteResult::Ok) { + return Ok(on_kind); + } + + promote.bound_names.push(binder.name); + let on_inner = check(promote, state, context, inner)?; + promote.bound_names.pop(); + + Ok(on_inner) + } + + Type::Constrained(constraint, inner) => check(promote, state, context, constraint)? + .and_then(|| check(promote, state, context, inner)), + + Type::Function(argument, result) => check(promote, state, context, argument)? + .and_then(|| check(promote, state, context, result)), + + Type::Kinded(inner, kind) => check(promote, state, context, inner)? + .and_then(|| check(promote, state, context, kind)), + + Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => Ok(PromoteResult::Ok), + Type::Integer(_) | Type::String(_, _) => Ok(PromoteResult::Ok), + + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + for field in row.fields.iter() { + let on_field = check(promote, state, context, field.id)?; + if !matches!(on_field, PromoteResult::Ok) { + return Ok(on_field); + } + } + if let Some(tail) = row.tail { + check(promote, state, context, tail) + } else { + Ok(PromoteResult::Ok) + } + } + + Type::Rigid(name, rigid_depth, kind) => { + if promote.bound_names.contains(&name) { + check(promote, state, context, kind) + } else if rigid_depth > promote.unification_depth { + Ok(PromoteResult::SkolemEscape) + } else { + check(promote, state, context, kind) + } + } + + Type::Unification(id) => { + // Disallow `?t := ?t` + if id == promote.unification_id { + return Ok(PromoteResult::OccursCheck); + } + // When solving `a` to a deeper unification variable `b`, + // + // ?a[0] := ?b[1] + // + // we create a fresh variable `c` as shallow as `a`, + // + // ?b[1] := ?c[0] + // + // which would be pruned into the final solution + // + // ?a[0] := ?c[0] + // + // This process eliminates unsolved unification variables + // at depth markers that are not in scope, and replaces + // them with unification variables that are in scope. + let UnificationEntry { depth, kind, .. } = *state.unifications.get(id); + if depth > promote.unification_depth { + let promoted = state.unifications.fresh(promote.unification_depth, kind); + let promoted = context.queries.intern_type(Type::Unification(promoted)); + state.unifications.solve(id, promoted); + } + + Ok(PromoteResult::Ok) + } + + Type::Free(_) | Type::Unknown(_) => Ok(PromoteResult::Ok), + } + } + + let unification_depth = state.unifications.get(unification_id).depth; + let bound_names = vec![]; + + let mut promote = PromotionState { unification_id, unification_depth, bound_names }; + check(&mut promote, state, context, solution) } fn unify_rows( From 2fdb0d2f27ce37fa1a90c3243f4c4de4a57daedf Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 02:02:33 +0800 Subject: [PATCH 191/386] Add text field to ForallBinder --- compiler-core/checking2/src/core.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index dd9a5dce..79c74d46 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -32,12 +32,14 @@ impl Depth { } /// Carries information about a type variable under a forall. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ForallBinder { /// Whether this binder is visible to type applications. pub visible: bool, /// The unique identity attached to the type variable. pub name: Name, + /// The source-level text of the type variable. + pub text: SmolStr, /// The kind of the type variable. pub kind: TypeId, } From 23b6f8a299a2926a39957e2e555a924272a78018 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 02:28:27 +0800 Subject: [PATCH 192/386] Implement pretty-printer for core --- Cargo.lock | 1 + compiler-core/checking2/Cargo.toml | 1 + compiler-core/checking2/src/core.rs | 1 + compiler-core/checking2/src/core/pretty.rs | 581 +++++++++++++++++++++ 4 files changed, 584 insertions(+) create mode 100644 compiler-core/checking2/src/core/pretty.rs diff --git a/Cargo.lock b/Cargo.lock index 8a5b3726..b18a47f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,7 @@ dependencies = [ "itertools 0.14.0", "lowering", "parsing", + "pretty", "resolving", "rustc-hash 2.1.1", "smol_str", diff --git a/compiler-core/checking2/Cargo.toml b/compiler-core/checking2/Cargo.toml index 71683527..8cb17df1 100644 --- a/compiler-core/checking2/Cargo.toml +++ b/compiler-core/checking2/Cargo.toml @@ -10,6 +10,7 @@ indexing = { version = "0.1.0", path = "../indexing" } interner = { version = "0.1.0", path = "../interner" } itertools = "0.14.0" lowering = { version = "0.1.0", path = "../lowering" } +pretty = "0.12" parsing = { version = "0.1.0", path = "../parsing" } resolving = { version = "0.1.0", path = "../resolving" } rustc-hash = "2.1.1" diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 79c74d46..d5f34a2e 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -3,6 +3,7 @@ pub mod fold; pub mod generalise; pub mod normalise; +pub mod pretty; pub mod substitute; pub mod unification; pub mod walk; diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs new file mode 100644 index 00000000..20880f1f --- /dev/null +++ b/compiler-core/checking2/src/core/pretty.rs @@ -0,0 +1,581 @@ +//! Implements the pretty printer for core types. + +use itertools::Itertools; +use lowering::StringKind; +use pretty::{Arena, DocAllocator, DocBuilder}; +use rustc_hash::FxHashMap; +use smol_str::{SmolStr, SmolStrBuilder}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{ + ForallBinder, ForallBinderId, Name, RowField, RowType, RowTypeId, SmolStrId, Synonym, + SynonymId, Type, TypeId, +}; +use crate::state::CheckState; + +type Doc<'a> = DocBuilder<'a, Arena<'a>, ()>; + +pub struct PrettyConfig { + pub width: usize, +} + +impl Default for PrettyConfig { + fn default() -> PrettyConfig { + PrettyConfig { width: 100 } + } +} + +pub fn print(state: &mut CheckState, context: &CheckContext, id: TypeId) -> SmolStr +where + Q: ExternalQueries, +{ + render(state, context, &PrettyConfig::default(), |printer| { + printer.traverse(Precedence::Top, id) + }) +} + +pub fn print_with_config( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + config: &PrettyConfig, +) -> SmolStr +where + Q: ExternalQueries, +{ + render(state, context, config, |printer| printer.traverse(Precedence::Top, id)) +} + +pub fn print_signature( + state: &mut CheckState, + context: &CheckContext, + name: &str, + id: TypeId, +) -> SmolStr +where + Q: ExternalQueries, +{ + render(state, context, &PrettyConfig::default(), |printer| printer.signature(name, id)) +} + +pub fn print_signature_with_config( + state: &mut CheckState, + context: &CheckContext, + name: &str, + id: TypeId, + config: &PrettyConfig, +) -> SmolStr +where + Q: ExternalQueries, +{ + render(state, context, config, |printer| printer.signature(name, id)) +} + +fn render( + state: &mut CheckState, + context: &CheckContext, + config: &PrettyConfig, + f: impl for<'a> FnOnce(&mut Printer<'a, '_, Q>) -> Doc<'a>, +) -> SmolStr +where + Q: ExternalQueries, +{ + let arena = Arena::new(); + let traversal = TraversalContext::new(); + let mut printer = Printer::new(&arena, state, context, traversal); + + let document = f(&mut printer); + let mut output = SmolStrBuilder::new(); + document + .render_fmt(config.width, &mut output) + .expect("critical failure: failed to render type"); + + output.finish() +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Precedence { + Top, + Constraint, + Function, + Application, + Atom, +} + +struct TraversalContext { + names: FxHashMap, + next: u32, +} + +impl TraversalContext { + fn new() -> TraversalContext { + TraversalContext { names: FxHashMap::default(), next: 0 } + } + + fn render_name(&mut self, name: Name) -> String { + let unique = &mut self.next; + let name = self.names.entry(name).or_insert_with(|| { + let index = *unique; + *unique += 1; + format!("t{index}") + }); + String::clone(name) + } +} + +struct Printer<'a, 'q, Q> +where + Q: ExternalQueries, +{ + arena: &'a Arena<'a>, + state: &'a mut CheckState, + context: &'a CheckContext<'q, Q>, + traversal: TraversalContext, +} + +impl<'a, 'q, Q> Printer<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn new( + arena: &'a Arena<'a>, + state: &'a mut CheckState, + context: &'a CheckContext<'q, Q>, + traversal: TraversalContext, + ) -> Printer<'a, 'q, Q> { + Printer { arena, state, context, traversal } + } + + fn lookup_type(&mut self, id: TypeId) -> Type { + let id = crate::core::normalise::normalise(self.state, self.context, id).unwrap_or(id); + self.context.queries.lookup_type(id) + } + + fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder { + self.context.queries.lookup_forall_binder(id) + } + + fn lookup_row_type(&self, id: RowTypeId) -> RowType { + self.context.queries.lookup_row_type(id) + } + + fn lookup_synonym(&self, id: SynonymId) -> Synonym { + self.context.queries.lookup_synonym(id) + } + + fn lookup_smol_str(&self, id: SmolStrId) -> smol_str::SmolStr { + self.context.queries.lookup_smol_str(id) + } + + fn lookup_type_name( + &self, + file_id: files::FileId, + type_id: indexing::TypeItemId, + ) -> Option { + let indexed = self.context.queries.indexed(file_id).ok()?; + indexed.items[type_id].name.as_ref().map(|name| name.to_string()) + } + + fn parens_if(&self, condition: bool, doc: Doc<'a>) -> Doc<'a> { + if condition { self.arena.text("(").append(doc).append(self.arena.text(")")) } else { doc } + } + + fn signature(&mut self, name: &str, id: TypeId) -> Doc<'a> { + let signature = self.traverse(Precedence::Top, id); + let signature = self.arena.line().append(signature).nest(2); + self.arena.text(format!("{name} ::")).append(signature).group() + } + + fn traverse(&mut self, precedence: Precedence, id: TypeId) -> Doc<'a> { + match self.lookup_type(id) { + Type::Application(function, argument) => { + self.traverse_application(precedence, function, argument) + } + + Type::KindApplication(function, argument) => { + self.traverse_kind_application(precedence, function, argument) + } + + Type::OperatorApplication(file_id, type_id, left, right) => { + let operator = self + .lookup_type_name(file_id, type_id) + .unwrap_or_else(|| "".to_string()); + + let left = self.traverse(Precedence::Application, left); + let right = self.traverse(Precedence::Application, right); + + let operator = left.append(self.arena.text(format!(" {operator} "))).append(right); + self.parens_if(precedence > Precedence::Application, operator) + } + + Type::SynonymApplication(synonym_id) => { + let synonym = self.lookup_synonym(synonym_id); + let (file_id, type_id) = synonym.reference; + let function = self + .lookup_type_name(file_id, type_id) + .unwrap_or_else(|| "".to_string()); + + if synonym.arguments.is_empty() { + return self.arena.text(function); + } + + let function = self.arena.text(function); + + let arguments = synonym + .arguments + .iter() + .map(|&argument| self.traverse(Precedence::Atom, argument)) + .collect_vec(); + + let arguments = + arguments.into_iter().fold(self.arena.nil(), |builder, argument| { + builder.append(self.arena.line()).append(argument) + }); + + let synonym = function.append(arguments.nest(2)).group(); + self.parens_if(precedence > Precedence::Application, synonym) + } + + Type::Forall(binder_id, inner) => self.traverse_forall(precedence, binder_id, inner), + + Type::Constrained(constraint, inner) => { + self.traverse_constrained(precedence, constraint, inner) + } + + Type::Function(argument, result) => { + self.traverse_function(precedence, argument, result) + } + + Type::Kinded(inner, kind) => { + let inner = self.traverse(Precedence::Application, inner); + let kind = self.traverse(Precedence::Top, kind); + let kinded = inner.append(self.arena.text(" :: ")).append(kind); + self.parens_if(precedence > Precedence::Atom, kinded) + } + + Type::Constructor(file_id, type_id) => { + let name = self + .lookup_type_name(file_id, type_id) + .unwrap_or_else(|| "".to_string()); + self.arena.text(name) + } + + Type::OperatorConstructor(file_id, type_id) => { + let name = self + .lookup_type_name(file_id, type_id) + .unwrap_or_else(|| "".to_string()); + self.arena.text(name) + } + + Type::Integer(integer) => { + let negative = integer.is_negative(); + let integer = self.arena.text(format!("{integer}")); + self.parens_if(negative, integer) + } + + Type::String(kind, string_id) => { + let string = self.lookup_smol_str(string_id); + match kind { + StringKind::String => self.arena.text(format!("\"{string}\"")), + StringKind::RawString => self.arena.text(format!("\"\"\"{string}\"\"\"")), + } + } + + Type::Row(row_id) => { + let row = self.lookup_row_type(row_id); + if row.fields.is_empty() && row.tail.is_none() { + return self.arena.text("()"); + } + self.format_row(&row.fields, row.tail) + } + + Type::Rigid(name, _, kind) => { + let name = self.traversal.render_name(name); + let kind = self.traverse(Precedence::Top, kind); + self.arena.text(format!("({name} :: ")).append(kind).append(self.arena.text(")")) + } + + Type::Unification(unification_id) => { + let entry = self.state.unifications.get(unification_id); + self.arena.text(format!("?{unification_id}[{}]", entry.depth.0)) + } + + Type::Free(name_id) => { + let name = self.lookup_smol_str(name_id); + self.arena.text(format!("{name}")) + } + + Type::Unknown(name_id) => { + let name = self.lookup_smol_str(name_id); + self.arena.text(format!("?[{name}]")) + } + } + } +} + +impl<'a, 'q, Q> Printer<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn traverse_application( + &mut self, + precedence: Precedence, + mut function: TypeId, + argument: TypeId, + ) -> Doc<'a> { + if function == self.context.prim.record { + return self.format_record_application(argument); + } + + let mut arguments = vec![argument]; + + while let Type::Application(inner_function, argument) = self.lookup_type(function) { + function = inner_function; + arguments.push(argument); + } + + let function = self.traverse(Precedence::Application, function); + + let arguments = arguments + .iter() + .rev() + .map(|&argument| self.traverse(Precedence::Atom, argument)) + .collect_vec(); + + let arguments = arguments.into_iter().fold(self.arena.nil(), |builder, argument| { + builder.append(self.arena.line()).append(argument) + }); + + let application = function.append(arguments.nest(2)).group(); + self.parens_if(precedence > Precedence::Application, application) + } + + fn format_record_application(&mut self, argument: TypeId) -> Doc<'a> { + match self.lookup_type(argument) { + Type::Row(row_id) => { + let row = self.lookup_row_type(row_id); + self.format_record(&row.fields, row.tail) + } + _ => { + let inner = self.traverse(Precedence::Top, argument); + self.arena.text("{| ").append(inner).append(self.arena.text(" }")) + } + } + } + + fn traverse_kind_application( + &mut self, + precedence: Precedence, + mut function: TypeId, + argument: TypeId, + ) -> Doc<'a> { + let mut arguments = vec![argument]; + + while let Type::KindApplication(inner_function, argument) = self.lookup_type(function) { + function = inner_function; + arguments.push(argument); + } + + let function = self.traverse(Precedence::Application, function); + + let arguments = arguments + .iter() + .rev() + .map(|&argument| self.traverse(Precedence::Atom, argument)) + .collect_vec(); + + let arguments = arguments.into_iter().fold(self.arena.nil(), |builder, argument| { + builder.append(self.arena.line()).append(self.arena.text("@")).append(argument) + }); + + let application = function.append(arguments.nest(2)).group(); + self.parens_if(precedence > Precedence::Application, application) + } +} + +impl<'a, 'q, Q> Printer<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn traverse_forall( + &mut self, + precedence: Precedence, + binder_id: ForallBinderId, + mut inner: TypeId, + ) -> Doc<'a> { + let binder = self.lookup_forall_binder(binder_id); + let mut binders = vec![binder]; + + while let Type::Forall(next_binder_id, next_inner) = self.lookup_type(inner) { + binders.push(self.lookup_forall_binder(next_binder_id)); + inner = next_inner; + } + + // Register source-level names so rigid variables in the body + // display their original names instead of synthetic ones. + for binder in &binders { + self.traversal.names.insert(binder.name, binder.text.to_string()); + } + + let binders = binders + .iter() + .map(|binder| { + let kind = self.traverse(Precedence::Top, binder.kind); + self.arena + .text(format!("({} :: ", binder.text)) + .append(kind) + .append(self.arena.text(")")) + .group() + }) + .collect_vec(); + + let mut binders = binders.into_iter(); + let binders = if let Some(first) = binders.next() { + binders.fold(first, |builder, binder| { + builder.append(self.arena.line().append(binder).nest(2).group()) + }) + } else { + self.arena.nil() + }; + + let header = self.arena.text("forall ").append(binders).append(self.arena.text(".")); + let inner = self.traverse(Precedence::Top, inner); + let inner = self.arena.line().append(inner).nest(2); + let forall = header.append(inner).group(); + + self.parens_if(precedence > Precedence::Top, forall) + } + + fn traverse_constrained( + &mut self, + precedence: Precedence, + constraint: TypeId, + mut inner: TypeId, + ) -> Doc<'a> { + let mut constraints = vec![constraint]; + + while let Type::Constrained(constraint, next_inner) = self.lookup_type(inner) { + constraints.push(constraint); + inner = next_inner; + } + + let constraints = constraints + .iter() + .map(|&constraint| self.traverse(Precedence::Application, constraint)) + .collect_vec(); + + let inner = self.traverse(Precedence::Constraint, inner); + + let arrow = self.arena.text(" =>").append(self.arena.line()); + let constraints = constraints.into_iter().fold(self.arena.nil(), |builder, constraint| { + builder.append(constraint).append(arrow.clone()) + }); + + let constraints = constraints.append(inner).group(); + self.parens_if(precedence > Precedence::Constraint, constraints) + } + + fn traverse_function( + &mut self, + precedence: Precedence, + argument: TypeId, + mut result: TypeId, + ) -> Doc<'a> { + let mut arguments = vec![argument]; + + while let Type::Function(argument, next_result) = self.lookup_type(result) { + result = next_result; + arguments.push(argument); + } + + let arguments = arguments + .iter() + .map(|&argument| self.traverse(Precedence::Application, argument)) + .collect_vec(); + + let result = self.traverse(Precedence::Function, result); + + let arrow = self.arena.text(" ->").append(self.arena.line()); + let arguments = arguments.into_iter().fold(self.arena.nil(), |builder, argument| { + builder.append(argument).append(arrow.clone()) + }); + + let function = arguments.append(result).group(); + self.parens_if(precedence > Precedence::Function, function) + } +} + +impl<'a, 'q, Q> Printer<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn format_record(&mut self, fields: &[RowField], tail: Option) -> Doc<'a> { + if fields.is_empty() && tail.is_none() { + return self.arena.text("{}"); + } + let body = self.format_row_body(fields, tail); + self.arena + .text("{ ") + .append(body) + .append(self.arena.line()) + .append(self.arena.text("}")) + .group() + } + + fn format_row(&mut self, fields: &[RowField], tail: Option) -> Doc<'a> { + let body = self.format_row_body(fields, tail); + self.arena + .text("( ") + .append(body) + .append(self.arena.line()) + .append(self.arena.text(")")) + .group() + } + + fn format_row_body(&mut self, fields: &[RowField], tail: Option) -> Doc<'a> { + if fields.is_empty() { + return if let Some(tail) = tail { + let tail = self.traverse(Precedence::Top, tail); + self.arena.text("| ").append(tail) + } else { + self.arena.nil() + }; + } + + let fields = fields + .iter() + .map(|field| { + let field_type = self.traverse(Precedence::Top, field.id); + (field.label.to_string(), field_type) + }) + .collect_vec(); + + let format_field = |arena: &'a Arena<'a>, label: String, field_type: Doc<'a>| { + let field_type = arena.line().append(field_type).nest(2).group(); + arena.text(format!("{label} ::")).append(field_type).align() + }; + + let mut fields = fields.into_iter(); + let (first_label, first_type) = fields.next().unwrap(); + let first = format_field(self.arena, first_label, first_type); + + let leading_comma = self.arena.hardline().append(self.arena.text(", ")); + let leading_comma = leading_comma.flat_alt(self.arena.text(", ")); + + let fields = fields.fold(first, |builder, (label, field_type)| { + builder + .append(leading_comma.clone()) + .append(format_field(self.arena, label, field_type)) + }); + + if let Some(tail) = tail { + let tail = self.traverse(Precedence::Top, tail); + let leading_pipe = self.arena.hardline().append(self.arena.text("| ")); + let leading_pipe = leading_pipe.flat_alt(self.arena.text(" | ")); + fields.append(leading_pipe).append(tail) + } else { + fields + } + } +} From e1d87ab0aeff6e1cbe4391df6c3e4cff00c66abb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 03:05:21 +0800 Subject: [PATCH 193/386] Use pretty printer in error messages --- .../checking2/src/core/unification.rs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index e398e6f7..4a1e98ff 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -5,7 +5,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Name, RowTypeId, Type, TypeId, normalise}; +use crate::core::{Depth, Name, RowTypeId, Type, TypeId, normalise, pretty}; use crate::error::ErrorKind; use crate::state::{CheckState, UnificationEntry}; @@ -133,8 +133,8 @@ where let unifies = match (t1_core, t2_core) { // TODO: document impredicativity - (Type::Unification(id), _) => return solve(state, context, id, t2), - (_, Type::Unification(id)) => return solve(state, context, id, t1), + (Type::Unification(id), _) => return solve(state, context, t1, id, t2), + (_, Type::Unification(id)) => return solve(state, context, t2, id, t1), ( Type::Application(t1_function, t1_argument), @@ -237,9 +237,12 @@ where }; if !unifies { - // TODO: pretty-print types for error messages - let t1 = context.queries.intern_smol_str("?".into()); - let t2 = context.queries.intern_smol_str("?".into()); + let t1 = pretty::print(state, context, t1); + let t1 = context.queries.intern_smol_str(t1); + + let t2 = pretty::print(state, context, t2); + let t2 = context.queries.intern_smol_str(t2); + state.insert_error(ErrorKind::CannotUnify { t1, t2 }); } @@ -250,7 +253,8 @@ where pub fn solve( state: &mut CheckState, context: &CheckContext, - unification_id: u32, + unification: TypeId, + id: u32, solution: TypeId, ) -> QueryResult where @@ -258,29 +262,25 @@ where { let solution = normalise::normalise(state, context, solution)?; - match promote_type(state, context, unification_id, solution)? { + match promote_type(state, context, id, solution)? { PromoteResult::Ok => {} - PromoteResult::OccursCheck => { - // TODO: pretty-print types for error messages - let t1 = context.queries.intern_smol_str("?".into()); - let t2 = context.queries.intern_smol_str("?".into()); - state.insert_error(ErrorKind::CannotUnify { t1, t2 }); - return Ok(false); - } - PromoteResult::SkolemEscape => { - // TODO: pretty-print types for error messages - let t1 = context.queries.intern_smol_str("?".into()); - let t2 = context.queries.intern_smol_str("?".into()); + PromoteResult::OccursCheck | PromoteResult::SkolemEscape => { + let t1 = pretty::print(state, context, unification); + let t1 = context.queries.intern_smol_str(t1); + + let t2 = pretty::print(state, context, solution); + let t2 = context.queries.intern_smol_str(t2); + state.insert_error(ErrorKind::CannotUnify { t1, t2 }); return Ok(false); } } - let unification_kind = state.unifications.get(unification_id).kind; + let unification_kind = state.unifications.get(id).kind; // TODO: unify kinds once kind elaboration is available let _ = unification_kind; - state.unifications.solve(unification_id, solution); + state.unifications.solve(id, solution); Ok(true) } @@ -322,16 +322,16 @@ impl PromoteResult { fn promote_type( state: &mut CheckState, context: &CheckContext, - unification_id: u32, + id: u32, solution: TypeId, ) -> QueryResult where Q: ExternalQueries, { struct PromotionState { - unification_id: u32, - unification_depth: crate::core::Depth, - bound_names: Vec, + id: u32, + depth: Depth, + names: Vec, } fn check( @@ -374,9 +374,9 @@ where return Ok(on_kind); } - promote.bound_names.push(binder.name); + promote.names.push(binder.name); let on_inner = check(promote, state, context, inner)?; - promote.bound_names.pop(); + promote.names.pop(); Ok(on_inner) } @@ -409,9 +409,9 @@ where } Type::Rigid(name, rigid_depth, kind) => { - if promote.bound_names.contains(&name) { + if promote.names.contains(&name) { check(promote, state, context, kind) - } else if rigid_depth > promote.unification_depth { + } else if rigid_depth > promote.depth { Ok(PromoteResult::SkolemEscape) } else { check(promote, state, context, kind) @@ -420,7 +420,7 @@ where Type::Unification(id) => { // Disallow `?t := ?t` - if id == promote.unification_id { + if id == promote.id { return Ok(PromoteResult::OccursCheck); } // When solving `a` to a deeper unification variable `b`, @@ -439,8 +439,8 @@ where // at depth markers that are not in scope, and replaces // them with unification variables that are in scope. let UnificationEntry { depth, kind, .. } = *state.unifications.get(id); - if depth > promote.unification_depth { - let promoted = state.unifications.fresh(promote.unification_depth, kind); + if depth > promote.depth { + let promoted = state.unifications.fresh(promote.depth, kind); let promoted = context.queries.intern_type(Type::Unification(promoted)); state.unifications.solve(id, promoted); } @@ -452,10 +452,10 @@ where } } - let unification_depth = state.unifications.get(unification_id).depth; - let bound_names = vec![]; + let depth = state.unifications.get(id).depth; + let names = vec![]; - let mut promote = PromotionState { unification_id, unification_depth, bound_names }; + let mut promote = PromotionState { id, depth, names }; check(&mut promote, state, context, solution) } From 5b1ccf869537d040dd35382b073dec7dd6f4e585 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 03:14:12 +0800 Subject: [PATCH 194/386] Implement collection for implication constraints --- .../checking2/src/core/unification.rs | 8 +- compiler-core/checking2/src/implication.rs | 79 +++++++++++++++++++ compiler-core/checking2/src/lib.rs | 1 + compiler-core/checking2/src/state.rs | 18 +++++ 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 compiler-core/checking2/src/implication.rs diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 4a1e98ff..a94eec93 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -81,12 +81,8 @@ where } (Type::Constrained(constraint, inner), _) if mode == ElaborationMode::Yes => { - // TODO: implication constraints - // - // state.push_wanted(constraint); - // subtype_with_mode(state, context, inner, t2, mode) - let _ = (constraint, inner); - Ok(false) + state.push_wanted(constraint); + subtype_with_mode(state, context, mode, inner, t2) } ( diff --git a/compiler-core/checking2/src/implication.rs b/compiler-core/checking2/src/implication.rs new file mode 100644 index 00000000..c2482215 --- /dev/null +++ b/compiler-core/checking2/src/implication.rs @@ -0,0 +1,79 @@ +//! Implements the implication tree for constraint collection. + +use std::collections::VecDeque; +use std::ops::{Index, IndexMut}; + +use crate::core::TypeId; + +/// A unique identifier for an implication scope. +pub type ImplicationId = u32; + +/// A node in the implication tree. +#[derive(Default)] +pub struct Implication { + pub given: Vec, + pub wanted: VecDeque, + pub children: Vec, + pub parent: Option, +} + +impl Implication { + pub fn new(parent: Option) -> Implication { + Implication { parent, ..Implication::default() } + } +} + +pub struct Implications { + nodes: Vec, + current: ImplicationId, +} + +impl Implications { + pub fn new() -> Self { + Implications { nodes: vec![Implication::new(None)], current: 0 } + } + + pub fn current(&self) -> ImplicationId { + self.current + } + + pub fn current_mut(&mut self) -> &mut Implication { + &mut self.nodes[self.current as usize] + } + + pub fn push(&mut self) -> ImplicationId { + let parent = self.current; + let id = self.nodes.len() as ImplicationId; + self.nodes.push(Implication::new(Some(parent))); + self.nodes[parent as usize].children.push(id); + self.current = id; + id + } + + pub fn pop(&mut self, implication: ImplicationId) { + debug_assert_eq!(implication, self.current); + let parent = + self[implication].parent.expect("invariant violated: missing implication parent"); + self.current = parent; + } +} + +impl Default for Implications { + fn default() -> Implications { + Implications::new() + } +} + +impl Index for Implications { + type Output = Implication; + + fn index(&self, index: ImplicationId) -> &Self::Output { + &self.nodes[index as usize] + } +} + +impl IndexMut for Implications { + fn index_mut(&mut self, index: ImplicationId) -> &mut Self::Output { + &mut self.nodes[index as usize] + } +} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 54399731..ffd28310 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -1,6 +1,7 @@ pub mod context; pub mod core; pub mod error; +pub mod implication; pub mod safety; pub mod source; pub mod state; diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 1dd36e24..7a83807a 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -6,6 +6,7 @@ use files::FileId; use crate::core::{Depth, Name, Type, TypeId}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; +use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; /// Manages [`Name`] values for [`CheckState`]. @@ -79,6 +80,7 @@ pub struct CheckState { pub names: Names, pub unifications: Unifications, + pub implications: Implications, pub depth: Depth, pub crumbs: Vec, @@ -90,6 +92,7 @@ impl CheckState { checked: Default::default(), names: Names::new(file_id), unifications: Default::default(), + implications: Default::default(), depth: Depth(0), crumbs: Default::default(), } @@ -129,4 +132,19 @@ impl CheckState { let crumbs = self.crumbs.iter().copied().collect(); self.checked.errors.push(CheckError { kind, crumbs }); } + + pub fn push_wanted(&mut self, constraint: TypeId) { + self.implications.current_mut().wanted.push_back(constraint); + } + + pub fn push_given(&mut self, constraint: TypeId) { + self.implications.current_mut().given.push(constraint); + } + + pub fn with_implication(&mut self, f: impl FnOnce(&mut CheckState) -> T) -> T { + let id = self.implications.push(); + let result = f(self); + self.implications.pop(id); + result + } } From e4502cbe1daa1f74d71787c7dad64b110c0f59f8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 03:31:11 +0800 Subject: [PATCH 195/386] Implement subtyping and unification for rows --- .../checking2/src/core/unification.rs | 162 +++++++++++++++++- 1 file changed, 155 insertions(+), 7 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index a94eec93..b75f0d96 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -1,11 +1,15 @@ //! Implements the subtyping and unification algorithms. +use std::sync::Arc; + use building_types::QueryResult; +use itertools::{EitherOrBoth, Itertools}; +use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Depth, Name, RowTypeId, Type, TypeId, normalise, pretty}; +use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise, pretty}; use crate::error::ErrorKind; use crate::state::{CheckState, UnificationEntry}; @@ -464,9 +468,28 @@ fn unify_rows( where Q: ExternalQueries, { - // TODO: implement row unification - let _ = (state, context, t1, t2); - Ok(false) + let t1_row = context.lookup_row_type(t1); + let t2_row = context.lookup_row_type(t2); + + let (left_only, right_only, ok) = partition_row_fields(state, context, &t1_row, &t2_row)?; + + if !ok { + return Ok(false); + } + + if t1_row.tail.is_none() && t2_row.tail.is_none() { + return Ok(left_only.is_empty() && right_only.is_empty()); + } + + if t2_row.tail.is_none() && !left_only.is_empty() { + return Ok(false); + } + + if t1_row.tail.is_none() && !right_only.is_empty() { + return Ok(false); + } + + unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) } fn subtype_rows( @@ -479,7 +502,132 @@ fn subtype_rows( where Q: ExternalQueries, { - // TODO: implement row subtyping - let _ = (state, context, mode, t1, t2); - Ok(false) + let t1_row = context.lookup_row_type(t1); + let t2_row = context.lookup_row_type(t2); + + let (left_only, right_only, ok) = partition_row_fields_with( + state, + context, + &t1_row, + &t2_row, + |state, context, left, right| subtype_with_mode(state, context, mode, left, right), + )?; + + if !ok { + return Ok(false); + } + + let mut failed = false; + + if t1_row.tail.is_none() && !right_only.is_empty() { + let labels = Arc::from_iter(right_only.iter().map(|field| SmolStr::clone(&field.label))); + state.insert_error(ErrorKind::PropertyIsMissing { labels }); + failed = true; + } + + if t2_row.tail.is_none() && !left_only.is_empty() { + let labels = Arc::from_iter(left_only.iter().map(|field| SmolStr::clone(&field.label))); + state.insert_error(ErrorKind::AdditionalProperty { labels }); + failed = true; + } + + if failed { + return Ok(false); + } + + unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) +} + +fn partition_row_fields( + state: &mut CheckState, + context: &CheckContext, + t1_row: &RowType, + t2_row: &RowType, +) -> QueryResult<(Vec, Vec, bool)> +where + Q: ExternalQueries, +{ + partition_row_fields_with(state, context, t1_row, t2_row, unify) +} + +fn partition_row_fields_with( + state: &mut CheckState, + context: &CheckContext, + t1_row: &RowType, + t2_row: &RowType, + mut field_check: F, +) -> QueryResult<(Vec, Vec, bool)> +where + Q: ExternalQueries, + F: FnMut(&mut CheckState, &CheckContext, TypeId, TypeId) -> QueryResult, +{ + let mut extras_left = vec![]; + let mut extras_right = vec![]; + let mut ok = true; + + let t1_fields = t1_row.fields.iter(); + let t2_fields = t2_row.fields.iter(); + + for field in t1_fields.merge_join_by(t2_fields, |left, right| left.label.cmp(&right.label)) { + match field { + EitherOrBoth::Both(left, right) => { + if !field_check(state, context, left.id, right.id)? { + ok = false; + } + } + EitherOrBoth::Left(left) => extras_left.push(left.clone()), + EitherOrBoth::Right(right) => extras_right.push(right.clone()), + } + } + + Ok((extras_left, extras_right, ok)) +} + +fn unify_row_tails( + state: &mut CheckState, + context: &CheckContext, + t1_tail: Option, + t2_tail: Option, + extras_left: Vec, + extras_right: Vec, +) -> QueryResult +where + Q: ExternalQueries, +{ + match (t1_tail, t2_tail) { + (None, None) => Ok(true), + + (Some(t1_tail), None) => { + let row = RowType::new(extras_right, None); + let row_id = context.intern_row_type(row); + let row_ty = context.intern_row(row_id); + unify(state, context, t1_tail, row_ty) + } + + (None, Some(t2_tail)) => { + let row = RowType::new(extras_left, None); + let row_id = context.intern_row_type(row); + let row_ty = context.intern_row(row_id); + unify(state, context, t2_tail, row_ty) + } + + (Some(t1_tail), Some(t2_tail)) => { + if extras_left.is_empty() && extras_right.is_empty() { + return unify(state, context, t1_tail, t2_tail); + } + + let tail = Some(state.fresh_unification(context.queries, context.prim.row_type)); + + let left_tail_row = RowType::new(extras_right, tail); + let left_tail_row_id = context.intern_row_type(left_tail_row); + let left_tail_ty = context.intern_row(left_tail_row_id); + + let right_tail_row = RowType::new(extras_left, tail); + let right_tail_row_id = context.intern_row_type(right_tail_row); + let right_tail_ty = context.intern_row(right_tail_row_id); + + Ok(unify(state, context, t1_tail, left_tail_ty)? + && unify(state, context, t2_tail, right_tail_ty)?) + } + } } From 9bede66cdf8bab58e3789f54cee4e9bee77e76bd Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 10:23:24 +0800 Subject: [PATCH 196/386] Add utility for pretty-print-intern --- compiler-core/checking2/src/core/unification.rs | 16 +++++----------- compiler-core/checking2/src/state.rs | 14 +++++++++++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index b75f0d96..e5c9a8be 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -9,7 +9,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise, pretty}; +use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise}; use crate::error::ErrorKind; use crate::state::{CheckState, UnificationEntry}; @@ -237,11 +237,8 @@ where }; if !unifies { - let t1 = pretty::print(state, context, t1); - let t1 = context.queries.intern_smol_str(t1); - - let t2 = pretty::print(state, context, t2); - let t2 = context.queries.intern_smol_str(t2); + let t1 = state.pretty_id(context, t1); + let t2 = state.pretty_id(context, t2); state.insert_error(ErrorKind::CannotUnify { t1, t2 }); } @@ -265,11 +262,8 @@ where match promote_type(state, context, id, solution)? { PromoteResult::Ok => {} PromoteResult::OccursCheck | PromoteResult::SkolemEscape => { - let t1 = pretty::print(state, context, unification); - let t1 = context.queries.intern_smol_str(t1); - - let t2 = pretty::print(state, context, solution); - let t2 = context.queries.intern_smol_str(t2); + let t1 = state.pretty_id(context, unification); + let t2 = state.pretty_id(context, solution); state.insert_error(ErrorKind::CannotUnify { t1, t2 }); return Ok(false); diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 7a83807a..78c3c0aa 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -4,7 +4,7 @@ use std::mem; use files::FileId; -use crate::core::{Depth, Name, Type, TypeId}; +use crate::core::{Depth, Name, SmolStrId, Type, TypeId}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; @@ -147,4 +147,16 @@ impl CheckState { self.implications.pop(id); result } + + pub fn pretty_id( + &mut self, + context: &crate::context::CheckContext, + id: TypeId, + ) -> SmolStrId + where + Q: ExternalQueries, + { + let pretty = crate::core::pretty::print(self, context, id); + context.queries.intern_smol_str(pretty) + } } From bbf92e42060f3052aa85f350e0b7cf938fed17c7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 03:57:44 +0800 Subject: [PATCH 197/386] Implement syntax-driven kind checking rules --- compiler-core/checking2/src/context.rs | 7 + compiler-core/checking2/src/source/types.rs | 482 +++++++++++++++++++- compiler-core/checking2/src/state.rs | 39 ++ 3 files changed, 522 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index 2174b0cd..d53f66f3 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -9,6 +9,7 @@ use files::FileId; use indexing::{IndexedModule, TermItemId, TypeItemId}; use lowering::{GroupedModule, LoweredModule}; use resolving::ResolvedModule; +use smol_str::SmolStr; use stabilizing::StabilizedModule; use sugar::{Bracketed, Sectioned}; @@ -119,6 +120,12 @@ impl<'q, Q> CheckContext<'q, Q> where Q: ExternalQueries, { + /// Creates an [`Type::Unknown`] type with a descriptive label. + pub fn unknown(&self, label: &str) -> TypeId { + let label = self.queries.intern_smol_str(SmolStr::new(label)); + self.queries.intern_type(Type::Unknown(label)) + } + /// Interns a [`Type::Application`] node. pub fn intern_application(&self, function: TypeId, argument: TypeId) -> TypeId { self.queries.intern_type(Type::Application(function, argument)) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 17511d81..3c10f35b 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -1,23 +1,493 @@ //! Implements syntax-driven checking rules for types. +use std::sync::Arc; + use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::TypeId; +use crate::core::substitute::SubstituteName; +use crate::core::unification; +use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::safe_loop; use crate::state::CheckState; +const MISSING_NAME: SmolStr = SmolStr::new_static(""); + /// Checks the kind of a syntax type against a core type. /// /// This function returns the core type and kind. pub fn check_kind( - _state: &mut CheckState, - _context: &CheckContext, - _source_type: lowering::TypeId, - _expected_kind: TypeId, + state: &mut CheckState, + context: &CheckContext, + source_type: lowering::TypeId, + expected_kind: TypeId, ) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { - todo!() + state.with_error_crumb(ErrorCrumb::CheckingKind(source_type), |state| { + let (inferred_type, inferred_kind) = infer_kind(state, context, source_type)?; + let (inferred_type, inferred_kind) = instantiate_kind_applications( + state, + context, + inferred_type, + inferred_kind, + expected_kind, + )?; + + unification::subtype(state, context, inferred_kind, expected_kind)?; + Ok((inferred_type, inferred_kind)) + }) +} + +fn infer_kind( + state: &mut CheckState, + context: &CheckContext, + id: lowering::TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::InferringKind(id), |state| { + let unknown = |message: &str| { + let u = context.unknown(message); + (u, u) + }; + + let Some(kind) = context.lowered.info.get_type_kind(id) else { + return Ok(unknown("missing syntax")); + }; + + match kind { + lowering::TypeKind::ApplicationChain { function, arguments } => { + let Some(function) = function else { + return Ok(unknown("missing application function")); + }; + + // Synonym detection is deferred. + todo!("synonym detection in ApplicationChain"); + + #[allow(unreachable_code)] + { + let (mut t, mut k) = infer_kind(state, context, *function)?; + + for argument in arguments.iter() { + (t, k) = infer_application_kind(state, context, (t, k), *argument)?; + } + + Ok((t, k)) + } + } + + lowering::TypeKind::Arrow { argument, result } => { + let argument = if let Some(argument) = argument { + let (argument, _) = check_kind(state, context, *argument, context.prim.t)?; + argument + } else { + context.unknown("missing function argument") + }; + + let result = if let Some(result) = result { + let (result, _) = check_kind(state, context, *result, context.prim.t)?; + result + } else { + context.unknown("missing function result") + }; + + let t = context.intern_function(argument, result); + let k = context.prim.t; + + Ok((t, k)) + } + + lowering::TypeKind::Constrained { constraint, constrained } => { + let constraint = if let Some(constraint) = constraint { + let (constraint, _) = + check_kind(state, context, *constraint, context.prim.constraint)?; + constraint + } else { + context.unknown("missing constraint") + }; + + let constrained = if let Some(constrained) = constrained { + // TODO: allow `Constraint` in this position + let (constrained, _) = + check_kind(state, context, *constrained, context.prim.t)?; + constrained + } else { + context.unknown("missing constrained") + }; + + let t = context.intern_constrained(constraint, constrained); + let k = context.prim.t; + + Ok((t, k)) + } + + lowering::TypeKind::Constructor { resolution } => { + let Some((file_id, type_id)) = *resolution else { + return Ok(unknown("missing constructor")); + }; + + // Synonym detection is deferred. + todo!("synonym detection in Constructor"); + + #[allow(unreachable_code)] + { + let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let k = lookup_file_type(state, context, file_id, type_id)?; + + Ok((t, k)) + } + } + + lowering::TypeKind::Forall { bindings, inner } => { + let binders = bindings + .iter() + .map(|binding| check_type_variable_binding(state, context, binding)) + .collect::>>()?; + + let inner = if let Some(inner) = inner { + // TODO: allow `Constraint` in this position + let (inner, _) = check_kind(state, context, *inner, context.prim.t)?; + inner + } else { + context.unknown("missing forall inner") + }; + + let t = binders.iter().rfold(inner, |inner, binder| { + let binder_id = context.intern_forall_binder(binder.clone()); + context.intern_forall(binder_id, inner) + }); + + let k = context.prim.t; + + Ok((t, k)) + } + + lowering::TypeKind::Hole => { + let k = state.fresh_unification(context.queries, context.prim.t); + let t = state.fresh_unification(context.queries, k); + Ok((t, k)) + } + + lowering::TypeKind::Integer { value } => { + let t = if let Some(value) = value { + context.queries.intern_type(Type::Integer(*value)) + } else { + context.unknown("missing integer value") + }; + Ok((t, context.prim.int)) + } + + lowering::TypeKind::Kinded { type_, kind } => { + let k = if let Some(kind) = kind { + let (k, _) = infer_kind(state, context, *kind)?; + k + } else { + context.unknown("missing kinded kind") + }; + let t = if let Some(type_) = type_ { + let (t, _) = check_kind(state, context, *type_, k)?; + t + } else { + context.unknown("missing kinded type") + }; + Ok((t, k)) + } + + lowering::TypeKind::Operator { resolution } => { + let Some((file_id, type_id)) = *resolution else { + return Ok(unknown("missing operator")); + }; + + let t = context.queries.intern_type(Type::OperatorConstructor(file_id, type_id)); + let k = lookup_file_type(state, context, file_id, type_id)?; + + Ok((t, k)) + } + + lowering::TypeKind::OperatorChain { .. } => { + todo!("operator chain inference") + } + + lowering::TypeKind::String { kind, value } => { + let value = value.clone().unwrap_or(MISSING_NAME); + let id = context.queries.intern_smol_str(value); + + let t = context.queries.intern_type(Type::String(*kind, id)); + let k = context.prim.symbol; + + Ok((t, k)) + } + + lowering::TypeKind::Variable { name, resolution } => match resolution { + Some(lowering::TypeVariableResolution::Forall(forall)) => { + let (n, k) = state + .kind_scope + .lookup_forall(*forall) + .expect("invariant violated: KindScope::bind_forall"); + + let t = context.intern_rigid(n, state.depth, k); + + Ok((t, k)) + } + + Some(lowering::TypeVariableResolution::Implicit(implicit)) => { + if implicit.binding { + let n = state.names.fresh(); + let k = state.fresh_unification(context.queries, context.prim.t); + + state.kind_scope.bind_implicit(implicit.node, implicit.id, n, k); + let t = context.intern_rigid(n, state.depth, k); + + Ok((t, k)) + } else { + let (n, k) = state + .kind_scope + .lookup_implicit(implicit.node, implicit.id) + .expect("invariant violated: KindScope::bind_implicit"); + + let t = context.intern_rigid(n, state.depth, k); + + Ok((t, k)) + } + } + + None => { + let name = name.clone().unwrap_or(MISSING_NAME); + let id = context.queries.intern_smol_str(name); + + let t = context.queries.intern_type(Type::Free(id)); + let k = state.fresh_unification(context.queries, context.prim.t); + + Ok((t, k)) + } + }, + + lowering::TypeKind::Wildcard => { + let k = state.fresh_unification(context.queries, context.prim.t); + let t = state.fresh_unification(context.queries, k); + Ok((t, k)) + } + + lowering::TypeKind::Record { items, tail } => { + let (row_type, row_kind) = infer_row_kind(state, context, items, tail)?; + unification::subtype(state, context, row_kind, context.prim.row_type)?; + + let t = context.intern_application(context.prim.record, row_type); + let k = context.prim.t; + + Ok((t, k)) + } + + lowering::TypeKind::Row { items, tail } => infer_row_kind(state, context, items, tail), + + lowering::TypeKind::Parenthesized { parenthesized } => { + if let Some(parenthesized) = parenthesized { + infer_kind(state, context, *parenthesized) + } else { + Ok(unknown("missing parenthesized")) + } + } + } + }) +} + +/// Instantiates kind-level foralls using [`Type::KindApplication`]. +/// +/// If the inferred kind is polymorphic and the expected kind is monomorphic, +/// this function adds the necessary kind applications to the inferred type. +fn instantiate_kind_applications( + state: &mut CheckState, + context: &CheckContext, + mut t: TypeId, + mut k: TypeId, + expected_kind: TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let expected_kind = normalise::normalise(state, context, expected_kind)?; + + if matches!(context.lookup_type(expected_kind), Type::Forall(_, _)) { + return Ok((t, k)); + } + + safe_loop! { + k = normalise::normalise(state, context, k)?; + + let Type::Forall(binder_id, inner_kind) = context.lookup_type(k) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let argument_type = state.fresh_unification(context.queries, binder_kind); + t = context.intern_kind_application(t, argument_type); + k = SubstituteName::one(state, context, binder.name, argument_type, inner_kind)?; + } + + Ok((t, k)) +} + +fn lookup_file_type( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let result = if file_id == context.id { + state.checked.types.get(&type_id).copied() + } else { + let checked = context.queries.checked(file_id)?; + checked.types.get(&type_id).copied() + }; + Ok(result.unwrap_or_else(|| context.unknown("kind"))) +} + +fn check_type_variable_binding( + state: &mut CheckState, + context: &CheckContext, + binding: &lowering::TypeVariableBinding, +) -> QueryResult +where + Q: ExternalQueries, +{ + let text = binding.name.clone().unwrap_or(MISSING_NAME); + + let kind = if let Some(kind_id) = binding.kind { + let (kind, _) = check_kind(state, context, kind_id, context.prim.t)?; + kind + } else { + state.fresh_unification(context.queries, context.prim.t) + }; + + let name = state.names.fresh(); + state.kind_scope.bind_forall(binding.id, name, kind); + + Ok(ForallBinder { visible: binding.visible, name, text, kind }) +} + +fn infer_application_kind( + state: &mut CheckState, + context: &CheckContext, + (function_type, function_kind): (TypeId, TypeId), + argument: lowering::TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let function_kind = normalise::normalise(state, context, function_kind)?; + + match context.lookup_type(function_kind) { + Type::Function(argument_kind, result_kind) => { + let (argument_type, _) = check_kind(state, context, argument, argument_kind)?; + + let t = context.intern_application(function_type, argument_type); + let k = normalise::normalise(state, context, result_kind)?; + + Ok((t, k)) + } + + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + + let function_u = context.intern_function(argument_u, result_u); + unification::solve(state, context, function_kind, unification_id, function_u)?; + + let (argument_type, _) = check_kind(state, context, argument, argument_u)?; + + let t = context.intern_application(function_type, argument_type); + let k = normalise::normalise(state, context, result_u)?; + + Ok((t, k)) + } + + Type::Forall(binder_id, inner_kind) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let kind_argument = state.fresh_unification(context.queries, binder_kind); + let function_type = context.intern_kind_application(function_type, kind_argument); + let function_kind = + SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; + + infer_application_kind(state, context, (function_type, function_kind), argument) + } + + _ => { + // Even if the function type cannot be applied, the argument must + // still be inferred. For invalid applications on instance heads, + // this ensures that implicit variables are bound. + let (argument_type, _) = infer_kind(state, context, argument)?; + + let t = context.intern_application(function_type, argument_type); + let k = context.unknown("function type cannot be applied"); + + let function_type = state.pretty_id(context, function_type); + let function_kind = state.pretty_id(context, function_kind); + let argument_type = state.pretty_id(context, argument_type); + + state.insert_error(ErrorKind::InvalidTypeApplication { + function_type, + function_kind, + argument_type, + }); + + Ok((t, k)) + } + } +} + +fn infer_row_kind( + state: &mut CheckState, + context: &CheckContext, + items: &Arc<[lowering::TypeRowItem]>, + tail: &Option, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let field_kind = state.fresh_unification(context.queries, context.prim.t); + let row_kind = context.intern_application(context.prim.row, field_kind); + + let fields = items.iter().map(|item| { + let label = item.name.clone().unwrap_or(MISSING_NAME); + let id = if let Some(t) = item.type_ { + let (t, k) = infer_kind(state, context, t)?; + unification::unify(state, context, field_kind, k)?; + t + } else { + context.unknown("missing field type") + }; + + Ok(RowField { label, id }) + }); + + let fields = fields.collect::>>()?; + + let tail = if let Some(tail) = tail { + let (tail_type, tail_kind) = infer_kind(state, context, *tail)?; + unification::subtype(state, context, tail_kind, row_kind)?; + Some(tail_type) + } else { + None + }; + + let row = RowType::new(fields, tail); + let row_id = context.intern_row_type(row); + let row_type = context.intern_row(row_id); + + Ok((row_type, row_kind)) } diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 78c3c0aa..6c86823b 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -3,6 +3,7 @@ use std::mem; use files::FileId; +use rustc_hash::FxHashMap; use crate::core::{Depth, Name, SmolStrId, Type, TypeId}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; @@ -74,6 +75,42 @@ impl Unifications { } } +/// Tracks type variable bindings during kind inference. +#[derive(Default)] +pub struct KindScope { + forall_bindings: FxHashMap, + implicit_bindings: + FxHashMap<(lowering::GraphNodeId, lowering::ImplicitBindingId), (Name, TypeId)>, +} + +impl KindScope { + pub fn bind_forall(&mut self, id: lowering::TypeVariableBindingId, name: Name, kind: TypeId) { + self.forall_bindings.insert(id, (name, kind)); + } + + pub fn lookup_forall(&self, id: lowering::TypeVariableBindingId) -> Option<(Name, TypeId)> { + self.forall_bindings.get(&id).copied() + } + + pub fn bind_implicit( + &mut self, + node: lowering::GraphNodeId, + id: lowering::ImplicitBindingId, + name: Name, + kind: TypeId, + ) { + self.implicit_bindings.insert((node, id), (name, kind)); + } + + pub fn lookup_implicit( + &self, + node: lowering::GraphNodeId, + id: lowering::ImplicitBindingId, + ) -> Option<(Name, TypeId)> { + self.implicit_bindings.get(&(node, id)).copied() + } +} + /// The core state structure threaded through the algorithm. pub struct CheckState { pub checked: CheckedModule, @@ -81,6 +118,7 @@ pub struct CheckState { pub names: Names, pub unifications: Unifications, pub implications: Implications, + pub kind_scope: KindScope, pub depth: Depth, pub crumbs: Vec, @@ -93,6 +131,7 @@ impl CheckState { names: Names::new(file_id), unifications: Default::default(), implications: Default::default(), + kind_scope: Default::default(), depth: Depth(0), crumbs: Default::default(), } From 5d71fc2d9756f04a6bba2ff6af341ac68f4bb83e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 16:33:41 +0800 Subject: [PATCH 198/386] Implement detection for type synonym applications --- compiler-core/checking2/src/lib.rs | 5 + compiler-core/checking2/src/source.rs | 1 + compiler-core/checking2/src/source/synonym.rs | 275 ++++++++++++++++++ compiler-core/checking2/src/source/types.rs | 52 ++-- compiler-core/checking2/src/state.rs | 2 + 5 files changed, 312 insertions(+), 23 deletions(-) create mode 100644 compiler-core/checking2/src/source/synonym.rs diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index ffd28310..5e86f875 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -52,6 +52,7 @@ pub trait ExternalQueries: #[derive(Debug, Default, PartialEq, Eq)] pub struct CheckedModule { pub types: FxHashMap, + pub synonyms: FxHashMap, pub roles: FxHashMap>, pub errors: Vec, } @@ -61,6 +62,10 @@ impl CheckedModule { self.types.get(&id).copied() } + pub fn lookup_synonym(&self, id: TypeItemId) -> Option { + self.synonyms.get(&id).cloned() + } + pub fn lookup_roles(&self, id: TypeItemId) -> Option> { self.roles.get(&id).cloned() } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index e7589661..9ee26335 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -1,5 +1,6 @@ //! Implements syntax-driven checking rules for source files. +pub mod synonym; pub mod terms; pub mod types; diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs new file mode 100644 index 00000000..43b70887 --- /dev/null +++ b/compiler-core/checking2/src/source/synonym.rs @@ -0,0 +1,275 @@ +//! Implements syntax-driven checking rules for synonym detection. + +use std::sync::Arc; + +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Saturation, Synonym, Type, TypeId, normalise, unification}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::types; + +pub fn lookup_file_synonym( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let (synonym, kind) = if file_id == context.id { + (state.checked.lookup_synonym(type_id), state.checked.lookup_type(type_id)) + } else { + let checked = context.queries.checked(file_id)?; + (checked.lookup_synonym(type_id), checked.lookup_type(type_id)) + }; + + Ok(synonym.zip(kind)) +} + +pub fn parse_synonym_application( + state: &mut CheckState, + context: &CheckContext, + function: lowering::TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(lowering::TypeKind::Constructor { resolution }) = + context.lowered.info.get_type_kind(function) + else { + return Ok(None); + }; + + let Some((file_id, type_id)) = *resolution else { + return Ok(None); + }; + + let Some((synonym, kind)) = lookup_file_synonym(state, context, file_id, type_id)? else { + return Ok(None); + }; + + Ok(Some((file_id, type_id, synonym, kind))) +} + +pub fn infer_synonym_constructor( + state: &mut CheckState, + context: &CheckContext, + (file_id, item_id, mut synonym, kind): (FileId, TypeItemId, Synonym, TypeId), + id: lowering::TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + if is_recursive_synonym(context, file_id, item_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); + } + + let expected_arity = expected_synonym_arity(context, file_id, item_id)?; + if expected_arity > 0 { + state.insert_error(ErrorKind::PartialSynonymApplication { id }); + let unknown = context.unknown("partial synonym application"); + return Ok((unknown, unknown)); + } + + synonym.saturation = Saturation::Full; + synonym.arguments = Arc::default(); + + let synonym_id = context.intern_synonym(synonym); + let synonym_type = context.intern_synonym_application(synonym_id); + + Ok((synonym_type, kind)) +} + +pub fn infer_synonym_application( + state: &mut CheckState, + context: &CheckContext, + id: lowering::TypeId, + (file_id, type_id, synonym, kind): (FileId, TypeItemId, Synonym, TypeId), + arguments: &[lowering::TypeId], +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + if is_recursive_synonym(context, file_id, type_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id: type_id }); + } + + let expected_arity = expected_synonym_arity(context, file_id, type_id)?; + let actual_arity = arguments.len(); + + if actual_arity < expected_arity { + state.insert_error(ErrorKind::PartialSynonymApplication { id }); + let unknown = context.unknown("partial synonym application"); + return Ok((unknown, unknown)); + } + + let (synonym_arguments, excess_arguments) = arguments.split_at(expected_arity); + + let (argument_types, synonym_kind) = + infer_synonym_application_arguments(state, context, kind, synonym_arguments)?; + + let synonym = Synonym { + saturation: Saturation::Full, + reference: synonym.reference, + arguments: Arc::from(argument_types), + }; + + let synonym_id = context.intern_synonym(synonym); + let mut synonym_type = context.intern_synonym_application(synonym_id); + let mut synonym_kind = synonym_kind; + + for &argument in excess_arguments { + (synonym_type, synonym_kind) = + types::infer_application_kind(state, context, (synonym_type, synonym_kind), argument)?; + } + + Ok((synonym_type, synonym_kind)) +} + +fn infer_synonym_application_arguments( + state: &mut CheckState, + context: &CheckContext, + kind: TypeId, + arguments: &[lowering::TypeId], +) -> QueryResult<(Vec, TypeId)> +where + Q: ExternalQueries, +{ + let mut argument_types = vec![]; + let mut synonym_kind = kind; + + for &argument_id in arguments { + let (argument_type, result_kind) = + infer_synonym_application_argument(state, context, synonym_kind, argument_id)?; + + argument_types.push(argument_type); + synonym_kind = result_kind; + } + + Ok((argument_types, synonym_kind)) +} + +fn infer_synonym_application_argument( + state: &mut CheckState, + context: &CheckContext, + function_kind: TypeId, + argument: lowering::TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let function_kind = normalise::normalise(state, context, function_kind)?; + + match context.lookup_type(function_kind) { + Type::Function(argument_kind, result_kind) => { + let (argument_type, _) = types::check_kind(state, context, argument, argument_kind)?; + let result_kind = normalise::normalise(state, context, result_kind)?; + Ok((argument_type, result_kind)) + } + + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + + let function_u = context.intern_function(argument_u, result_u); + unification::solve(state, context, function_kind, unification_id, function_u)?; + + let (argument_type, _) = types::check_kind(state, context, argument, argument_u)?; + let result_kind = normalise::normalise(state, context, result_u)?; + + Ok((argument_type, result_kind)) + } + + Type::Forall(binder_id, inner_kind) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let kind_argument = state.fresh_unification(context.queries, binder_kind); + let function_kind = + SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; + + infer_synonym_application_argument(state, context, function_kind, argument) + } + + _ => { + let (argument_type, _) = types::infer_kind(state, context, argument)?; + + { + let function_type = state.pretty_id(context, context.unknown("synonym function")); + let function_kind = state.pretty_id(context, function_kind); + let argument_type = state.pretty_id(context, argument_type); + + state.insert_error(ErrorKind::InvalidTypeApplication { + function_type, + function_kind, + argument_type, + }); + } + + let unknown = context.unknown("synonym function kind cannot be applied"); + Ok((argument_type, unknown)) + } + } +} + +fn expected_synonym_arity( + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expected_arity = |lowered: &lowering::LoweredModule| { + let Some(lowering::TypeItemIr::SynonymGroup { synonym, .. }) = + lowered.info.get_type_item(type_id) + else { + return 0; + }; + let Some(synonym) = synonym else { + return 0; + }; + synonym.variables.len() + }; + if file_id == context.id { + Ok(expected_arity(&context.lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(expected_arity(&lowered)) + } +} + +fn is_recursive_synonym( + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let has_recursive_error = |grouped: &lowering::GroupedModule| { + grouped.cycle_errors.iter().any(|error| { + if let lowering::LoweringError::RecursiveSynonym(recursive) = error { + recursive.group.contains(&type_id) + } else { + false + } + }) + }; + + let grouped = if file_id == context.id { + Arc::clone(&context.grouped) + } else { + context.queries.grouped(file_id)? + }; + + Ok(has_recursive_error(&grouped)) +} diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 3c10f35b..4019a63b 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -7,14 +7,13 @@ use files::FileId; use indexing::TypeItemId; use smol_str::SmolStr; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::unification; -use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise}; +use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise, unification}; use crate::error::{ErrorCrumb, ErrorKind}; -use crate::safe_loop; +use crate::source::synonym; use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; const MISSING_NAME: SmolStr = SmolStr::new_static(""); @@ -45,7 +44,7 @@ where }) } -fn infer_kind( +pub fn infer_kind( state: &mut CheckState, context: &CheckContext, id: lowering::TypeId, @@ -69,19 +68,21 @@ where return Ok(unknown("missing application function")); }; - // Synonym detection is deferred. - todo!("synonym detection in ApplicationChain"); - - #[allow(unreachable_code)] + if let Some(synonym) = + synonym::parse_synonym_application(state, context, *function)? { - let (mut t, mut k) = infer_kind(state, context, *function)?; + return synonym::infer_synonym_application( + state, context, id, synonym, arguments, + ); + } - for argument in arguments.iter() { - (t, k) = infer_application_kind(state, context, (t, k), *argument)?; - } + let (mut t, mut k) = infer_kind(state, context, *function)?; - Ok((t, k)) + for argument in arguments.iter() { + (t, k) = infer_application_kind(state, context, (t, k), *argument)?; } + + Ok((t, k)) } lowering::TypeKind::Arrow { argument, result } => { @@ -134,16 +135,21 @@ where return Ok(unknown("missing constructor")); }; - // Synonym detection is deferred. - todo!("synonym detection in Constructor"); - - #[allow(unreachable_code)] + if let Some((synonym, kind)) = + synonym::lookup_file_synonym(state, context, file_id, type_id)? { - let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let k = lookup_file_type(state, context, file_id, type_id)?; - - Ok((t, k)) + let synonym = (file_id, type_id, synonym, kind); + return synonym::infer_synonym_constructor( + state, + context, + synonym, + id, + ); } + let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let k = lookup_file_type(state, context, file_id, type_id)?; + + Ok((t, k)) } lowering::TypeKind::Forall { bindings, inner } => { @@ -378,7 +384,7 @@ where Ok(ForallBinder { visible: binding.visible, name, text, kind }) } -fn infer_application_kind( +pub fn infer_application_kind( state: &mut CheckState, context: &CheckContext, (function_type, function_kind): (TypeId, TypeId), diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 6c86823b..2a71474f 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -119,6 +119,7 @@ pub struct CheckState { pub unifications: Unifications, pub implications: Implications, pub kind_scope: KindScope, + pub defer_synonym_expansion: bool, pub depth: Depth, pub crumbs: Vec, @@ -132,6 +133,7 @@ impl CheckState { unifications: Default::default(), implications: Default::default(), kind_scope: Default::default(), + defer_synonym_expansion: Default::default(), depth: Depth(0), crumbs: Default::default(), } From 28a92692fc83cf2e3b68a672edc90a4c2fda3f4e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 16:56:32 +0800 Subject: [PATCH 199/386] Move check_kind/infer_kind internals to _core functions --- compiler-core/checking2/src/source/types.rs | 424 ++++++++++---------- 1 file changed, 216 insertions(+), 208 deletions(-) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 4019a63b..e58d7150 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -30,20 +30,27 @@ where Q: ExternalQueries, { state.with_error_crumb(ErrorCrumb::CheckingKind(source_type), |state| { - let (inferred_type, inferred_kind) = infer_kind(state, context, source_type)?; - let (inferred_type, inferred_kind) = instantiate_kind_applications( - state, - context, - inferred_type, - inferred_kind, - expected_kind, - )?; - - unification::subtype(state, context, inferred_kind, expected_kind)?; - Ok((inferred_type, inferred_kind)) + check_kind_core(state, context, source_type, expected_kind) }) } +fn check_kind_core( + state: &mut CheckState, + context: &CheckContext, + source_type: lowering::TypeId, + expected_kind: TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let (inferred_type, inferred_kind) = infer_kind(state, context, source_type)?; + let (inferred_type, inferred_kind) = + instantiate_kind_applications(state, context, inferred_type, inferred_kind, expected_kind)?; + + unification::subtype(state, context, inferred_kind, expected_kind)?; + Ok((inferred_type, inferred_kind)) +} + pub fn infer_kind( state: &mut CheckState, context: &CheckContext, @@ -53,256 +60,257 @@ where Q: ExternalQueries, { state.with_error_crumb(ErrorCrumb::InferringKind(id), |state| { - let unknown = |message: &str| { - let u = context.unknown(message); - (u, u) - }; + infer_kind_core(state, context, id) + }) +} - let Some(kind) = context.lowered.info.get_type_kind(id) else { - return Ok(unknown("missing syntax")); - }; +fn infer_kind_core( + state: &mut CheckState, + context: &CheckContext, + id: lowering::TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let unknown = |message: &str| { + let u = context.unknown(message); + (u, u) + }; - match kind { - lowering::TypeKind::ApplicationChain { function, arguments } => { - let Some(function) = function else { - return Ok(unknown("missing application function")); - }; - - if let Some(synonym) = - synonym::parse_synonym_application(state, context, *function)? - { - return synonym::infer_synonym_application( - state, context, id, synonym, arguments, - ); - } + let Some(kind) = context.lowered.info.get_type_kind(id) else { + return Ok(unknown("missing syntax")); + }; - let (mut t, mut k) = infer_kind(state, context, *function)?; + match kind { + lowering::TypeKind::ApplicationChain { function, arguments } => { + let Some(function) = function else { + return Ok(unknown("missing application function")); + }; - for argument in arguments.iter() { - (t, k) = infer_application_kind(state, context, (t, k), *argument)?; - } + if let Some(synonym) = synonym::parse_synonym_application(state, context, *function)? { + return synonym::infer_synonym_application(state, context, id, synonym, arguments); + } - Ok((t, k)) + let (mut t, mut k) = infer_kind(state, context, *function)?; + + for argument in arguments.iter() { + (t, k) = infer_application_kind(state, context, (t, k), *argument)?; } - lowering::TypeKind::Arrow { argument, result } => { - let argument = if let Some(argument) = argument { - let (argument, _) = check_kind(state, context, *argument, context.prim.t)?; - argument - } else { - context.unknown("missing function argument") - }; + Ok((t, k)) + } - let result = if let Some(result) = result { - let (result, _) = check_kind(state, context, *result, context.prim.t)?; - result - } else { - context.unknown("missing function result") - }; + lowering::TypeKind::Arrow { argument, result } => { + let argument = if let Some(argument) = argument { + let (argument, _) = check_kind(state, context, *argument, context.prim.t)?; + argument + } else { + context.unknown("missing function argument") + }; - let t = context.intern_function(argument, result); - let k = context.prim.t; + let result = if let Some(result) = result { + let (result, _) = check_kind(state, context, *result, context.prim.t)?; + result + } else { + context.unknown("missing function result") + }; - Ok((t, k)) - } + let t = context.intern_function(argument, result); + let k = context.prim.t; - lowering::TypeKind::Constrained { constraint, constrained } => { - let constraint = if let Some(constraint) = constraint { - let (constraint, _) = - check_kind(state, context, *constraint, context.prim.constraint)?; - constraint - } else { - context.unknown("missing constraint") - }; - - let constrained = if let Some(constrained) = constrained { - // TODO: allow `Constraint` in this position - let (constrained, _) = - check_kind(state, context, *constrained, context.prim.t)?; - constrained - } else { - context.unknown("missing constrained") - }; + Ok((t, k)) + } - let t = context.intern_constrained(constraint, constrained); - let k = context.prim.t; + lowering::TypeKind::Constrained { constraint, constrained } => { + let constraint = if let Some(constraint) = constraint { + let (constraint, _) = + check_kind(state, context, *constraint, context.prim.constraint)?; + constraint + } else { + context.unknown("missing constraint") + }; + + let constrained = if let Some(constrained) = constrained { + // TODO: allow `Constraint` in this position + let (constrained, _) = check_kind(state, context, *constrained, context.prim.t)?; + constrained + } else { + context.unknown("missing constrained") + }; + + let t = context.intern_constrained(constraint, constrained); + let k = context.prim.t; - Ok((t, k)) - } + Ok((t, k)) + } - lowering::TypeKind::Constructor { resolution } => { - let Some((file_id, type_id)) = *resolution else { - return Ok(unknown("missing constructor")); - }; - - if let Some((synonym, kind)) = - synonym::lookup_file_synonym(state, context, file_id, type_id)? - { - let synonym = (file_id, type_id, synonym, kind); - return synonym::infer_synonym_constructor( - state, - context, - synonym, - id, - ); - } - let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let k = lookup_file_type(state, context, file_id, type_id)?; + lowering::TypeKind::Constructor { resolution } => { + let Some((file_id, type_id)) = *resolution else { + return Ok(unknown("missing constructor")); + }; - Ok((t, k)) + if let Some((synonym, kind)) = + synonym::lookup_file_synonym(state, context, file_id, type_id)? + { + let synonym = (file_id, type_id, synonym, kind); + return synonym::infer_synonym_constructor(state, context, synonym, id); } + let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let k = lookup_file_type(state, context, file_id, type_id)?; - lowering::TypeKind::Forall { bindings, inner } => { - let binders = bindings - .iter() - .map(|binding| check_type_variable_binding(state, context, binding)) - .collect::>>()?; + Ok((t, k)) + } - let inner = if let Some(inner) = inner { - // TODO: allow `Constraint` in this position - let (inner, _) = check_kind(state, context, *inner, context.prim.t)?; - inner - } else { - context.unknown("missing forall inner") - }; + lowering::TypeKind::Forall { bindings, inner } => { + let binders = bindings + .iter() + .map(|binding| check_type_variable_binding(state, context, binding)) + .collect::>>()?; + + let inner = if let Some(inner) = inner { + // TODO: allow `Constraint` in this position + let (inner, _) = check_kind(state, context, *inner, context.prim.t)?; + inner + } else { + context.unknown("missing forall inner") + }; + + let t = binders.iter().rfold(inner, |inner, binder| { + let binder_id = context.intern_forall_binder(binder.clone()); + context.intern_forall(binder_id, inner) + }); - let t = binders.iter().rfold(inner, |inner, binder| { - let binder_id = context.intern_forall_binder(binder.clone()); - context.intern_forall(binder_id, inner) - }); + let k = context.prim.t; - let k = context.prim.t; + Ok((t, k)) + } - Ok((t, k)) - } + lowering::TypeKind::Hole => { + let k = state.fresh_unification(context.queries, context.prim.t); + let t = state.fresh_unification(context.queries, k); + Ok((t, k)) + } - lowering::TypeKind::Hole => { - let k = state.fresh_unification(context.queries, context.prim.t); - let t = state.fresh_unification(context.queries, k); - Ok((t, k)) - } + lowering::TypeKind::Integer { value } => { + let t = if let Some(value) = value { + context.queries.intern_type(Type::Integer(*value)) + } else { + context.unknown("missing integer value") + }; + Ok((t, context.prim.int)) + } - lowering::TypeKind::Integer { value } => { - let t = if let Some(value) = value { - context.queries.intern_type(Type::Integer(*value)) - } else { - context.unknown("missing integer value") - }; - Ok((t, context.prim.int)) - } + lowering::TypeKind::Kinded { type_, kind } => { + let k = if let Some(kind) = kind { + let (k, _) = infer_kind(state, context, *kind)?; + k + } else { + context.unknown("missing kinded kind") + }; + let t = if let Some(type_) = type_ { + let (t, _) = check_kind(state, context, *type_, k)?; + t + } else { + context.unknown("missing kinded type") + }; + Ok((t, k)) + } - lowering::TypeKind::Kinded { type_, kind } => { - let k = if let Some(kind) = kind { - let (k, _) = infer_kind(state, context, *kind)?; - k - } else { - context.unknown("missing kinded kind") - }; - let t = if let Some(type_) = type_ { - let (t, _) = check_kind(state, context, *type_, k)?; - t - } else { - context.unknown("missing kinded type") - }; - Ok((t, k)) - } + lowering::TypeKind::Operator { resolution } => { + let Some((file_id, type_id)) = *resolution else { + return Ok(unknown("missing operator")); + }; - lowering::TypeKind::Operator { resolution } => { - let Some((file_id, type_id)) = *resolution else { - return Ok(unknown("missing operator")); - }; + let t = context.queries.intern_type(Type::OperatorConstructor(file_id, type_id)); + let k = lookup_file_type(state, context, file_id, type_id)?; - let t = context.queries.intern_type(Type::OperatorConstructor(file_id, type_id)); - let k = lookup_file_type(state, context, file_id, type_id)?; + Ok((t, k)) + } - Ok((t, k)) - } + lowering::TypeKind::OperatorChain { .. } => { + todo!("operator chain inference") + } - lowering::TypeKind::OperatorChain { .. } => { - todo!("operator chain inference") - } + lowering::TypeKind::String { kind, value } => { + let value = value.clone().unwrap_or(MISSING_NAME); + let id = context.queries.intern_smol_str(value); - lowering::TypeKind::String { kind, value } => { - let value = value.clone().unwrap_or(MISSING_NAME); - let id = context.queries.intern_smol_str(value); + let t = context.queries.intern_type(Type::String(*kind, id)); + let k = context.prim.symbol; - let t = context.queries.intern_type(Type::String(*kind, id)); - let k = context.prim.symbol; + Ok((t, k)) + } + + lowering::TypeKind::Variable { name, resolution } => match resolution { + Some(lowering::TypeVariableResolution::Forall(forall)) => { + let (n, k) = state + .kind_scope + .lookup_forall(*forall) + .expect("invariant violated: KindScope::bind_forall"); + + let t = context.intern_rigid(n, state.depth, k); Ok((t, k)) } - lowering::TypeKind::Variable { name, resolution } => match resolution { - Some(lowering::TypeVariableResolution::Forall(forall)) => { - let (n, k) = state - .kind_scope - .lookup_forall(*forall) - .expect("invariant violated: KindScope::bind_forall"); + Some(lowering::TypeVariableResolution::Implicit(implicit)) => { + if implicit.binding { + let n = state.names.fresh(); + let k = state.fresh_unification(context.queries, context.prim.t); + state.kind_scope.bind_implicit(implicit.node, implicit.id, n, k); let t = context.intern_rigid(n, state.depth, k); Ok((t, k)) - } - - Some(lowering::TypeVariableResolution::Implicit(implicit)) => { - if implicit.binding { - let n = state.names.fresh(); - let k = state.fresh_unification(context.queries, context.prim.t); - - state.kind_scope.bind_implicit(implicit.node, implicit.id, n, k); - let t = context.intern_rigid(n, state.depth, k); - - Ok((t, k)) - } else { - let (n, k) = state - .kind_scope - .lookup_implicit(implicit.node, implicit.id) - .expect("invariant violated: KindScope::bind_implicit"); - - let t = context.intern_rigid(n, state.depth, k); - - Ok((t, k)) - } - } - - None => { - let name = name.clone().unwrap_or(MISSING_NAME); - let id = context.queries.intern_smol_str(name); + } else { + let (n, k) = state + .kind_scope + .lookup_implicit(implicit.node, implicit.id) + .expect("invariant violated: KindScope::bind_implicit"); - let t = context.queries.intern_type(Type::Free(id)); - let k = state.fresh_unification(context.queries, context.prim.t); + let t = context.intern_rigid(n, state.depth, k); Ok((t, k)) } - }, + } - lowering::TypeKind::Wildcard => { + None => { + let name = name.clone().unwrap_or(MISSING_NAME); + let id = context.queries.intern_smol_str(name); + + let t = context.queries.intern_type(Type::Free(id)); let k = state.fresh_unification(context.queries, context.prim.t); - let t = state.fresh_unification(context.queries, k); + Ok((t, k)) } + }, - lowering::TypeKind::Record { items, tail } => { - let (row_type, row_kind) = infer_row_kind(state, context, items, tail)?; - unification::subtype(state, context, row_kind, context.prim.row_type)?; + lowering::TypeKind::Wildcard => { + let k = state.fresh_unification(context.queries, context.prim.t); + let t = state.fresh_unification(context.queries, k); + Ok((t, k)) + } - let t = context.intern_application(context.prim.record, row_type); - let k = context.prim.t; + lowering::TypeKind::Record { items, tail } => { + let (row_type, row_kind) = infer_row_kind(state, context, items, tail)?; + unification::subtype(state, context, row_kind, context.prim.row_type)?; - Ok((t, k)) - } + let t = context.intern_application(context.prim.record, row_type); + let k = context.prim.t; - lowering::TypeKind::Row { items, tail } => infer_row_kind(state, context, items, tail), + Ok((t, k)) + } - lowering::TypeKind::Parenthesized { parenthesized } => { - if let Some(parenthesized) = parenthesized { - infer_kind(state, context, *parenthesized) - } else { - Ok(unknown("missing parenthesized")) - } + lowering::TypeKind::Row { items, tail } => infer_row_kind(state, context, items, tail), + + lowering::TypeKind::Parenthesized { parenthesized } => { + if let Some(parenthesized) = parenthesized { + infer_kind(state, context, *parenthesized) + } else { + Ok(unknown("missing parenthesized")) } } - }) + } } /// Instantiates kind-level foralls using [`Type::KindApplication`]. From 6f5b2a59703c498f066483a70df3c188e6f0d3c7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 17:13:27 +0800 Subject: [PATCH 200/386] Allow Constraint kinds in Constraint and Forall This is a non-standard extension to PureScript that would enable checking for the representation that we'll be using for instance dictionaries. For example, we plan to represent the following: class F :: forall k. (k -> Type) -> Constraint class G :: forall k. (k -> Type) -> Constraint instance (F f, G f) => (Compose f g) forall {fk gk} (f :: fk -> Type) (g :: gk -> Type). F f => G f => Compose f g --- compiler-core/checking2/src/source/types.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index e58d7150..7c94d0af 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -131,8 +131,7 @@ where }; let constrained = if let Some(constrained) = constrained { - // TODO: allow `Constraint` in this position - let (constrained, _) = check_kind(state, context, *constrained, context.prim.t)?; + let (constrained, _) = infer_kind(state, context, *constrained)?; constrained } else { context.unknown("missing constrained") @@ -168,8 +167,7 @@ where .collect::>>()?; let inner = if let Some(inner) = inner { - // TODO: allow `Constraint` in this position - let (inner, _) = check_kind(state, context, *inner, context.prim.t)?; + let (inner, _) = infer_kind(state, context, *inner)?; inner } else { context.unknown("missing forall inner") From 468bf9ccc196548dcf7262ac96a860a0965842a9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 17:26:04 +0800 Subject: [PATCH 201/386] Add documentation to ElaborationMode --- compiler-core/checking2/src/core/unification.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index e5c9a8be..8d2f9914 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -14,6 +14,13 @@ use crate::error::ErrorKind; use crate::state::{CheckState, UnificationEntry}; /// Determines if constraints are elaborated during [`subtype`]. +/// +/// Elaboration involves pushing constraints as "wanted" and inserting +/// dictionary placeholders in the generated IR. This behaviour is only +/// wanted in positions where the type checker can insert dictionaries. +/// +/// This is disabled in the subtyping rule for [`Type::Function`] and the +/// checking rules for binders; it's impossible to add dictionaries there. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ElaborationMode { Yes, From fcea5e6de50f04562306536e449541afd177bff0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 17:52:55 +0800 Subject: [PATCH 202/386] Refactor subtyping to use a strategy pattern --- .../checking2/src/core/unification.rs | 96 ++++++++++++++----- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 8d2f9914..5e82ed64 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -13,18 +13,65 @@ use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, norma use crate::error::ErrorKind; use crate::state::{CheckState, UnificationEntry}; -/// Determines if constraints are elaborated during [`subtype`]. +/// Strategy for handling constrained types during [`subtype_with`]. /// -/// Elaboration involves pushing constraints as "wanted" and inserting -/// dictionary placeholders in the generated IR. This behaviour is only -/// wanted in positions where the type checker can insert dictionaries. +/// Elaboration involves pushing constraints as "wanted". This behaviour is +/// only wanted in positions where the type checker can insert dictionaries. /// -/// This is disabled in the subtyping rule for [`Type::Function`] and the -/// checking rules for binders; it's impossible to add dictionaries there. +/// For example in [`Type::Function`], subtyping is [`NonElaborating`] +/// because it's impossible to insert dictionaries there; the same is +/// true when checking [`lowering::BinderKind`] in arguments and patterns. +pub trait SubtypePolicy +where + Q: ExternalQueries, +{ + fn on_constrained( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + constraint: TypeId, + constrained: TypeId, + t2: TypeId, + ) -> QueryResult; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Elaborating; + +impl SubtypePolicy for Elaborating +where + Q: ExternalQueries, +{ + fn on_constrained( + state: &mut CheckState, + context: &CheckContext, + _t1: TypeId, + constraint: TypeId, + constrained: TypeId, + t2: TypeId, + ) -> QueryResult { + state.push_wanted(constraint); + subtype_with::(state, context, constrained, t2) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ElaborationMode { - Yes, - No, +pub struct NonElaborating; + +impl SubtypePolicy for NonElaborating +where + Q: ExternalQueries, +{ + fn on_constrained( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + _constraint: TypeId, + _constrained: TypeId, + t2: TypeId, + ) -> QueryResult { + unify(state, context, t1, t2) + } } pub fn subtype( @@ -36,17 +83,17 @@ pub fn subtype( where Q: ExternalQueries, { - subtype_with_mode(state, context, ElaborationMode::Yes, t1, t2) + subtype_with::(state, context, t1, t2) } -pub fn subtype_with_mode( +pub fn subtype_with( state: &mut CheckState, context: &CheckContext, - mode: ElaborationMode, t1: TypeId, t2: TypeId, ) -> QueryResult where + P: SubtypePolicy, Q: ExternalQueries, { let t1 = normalise::normalise(state, context, t1)?; @@ -61,18 +108,18 @@ where match (t1_core, t2_core) { (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { - Ok(subtype_with_mode(state, context, ElaborationMode::No, t2_argument, t1_argument)? - && subtype_with_mode(state, context, ElaborationMode::No, t1_result, t2_result)?) + Ok(subtype_with::(state, context, t2_argument, t1_argument)? + && subtype_with::(state, context, t1_result, t2_result)?) } (Type::Application(_, _), Type::Function(t2_argument, t2_result)) => { let t2 = context.intern_function_application(t2_argument, t2_result); - subtype_with_mode(state, context, mode, t1, t2) + subtype_with::(state, context, t1, t2) } (Type::Function(t1_argument, t1_result), Type::Application(_, _)) => { let t1 = context.intern_function_application(t1_argument, t1_result); - subtype_with_mode(state, context, mode, t1, t2) + subtype_with::(state, context, t1, t2) } (_, Type::Forall(binder_id, inner)) => { @@ -80,7 +127,7 @@ where let skolem = state.fresh_rigid(context.queries, binder.kind); let inner = SubstituteName::one(state, context, binder.name, skolem, inner)?; - state.with_depth(|state| subtype_with_mode(state, context, mode, t1, inner)) + state.with_depth(|state| subtype_with::(state, context, t1, inner)) } (Type::Forall(binder_id, inner), _) => { @@ -88,12 +135,11 @@ where let unification = state.fresh_unification(context.queries, binder.kind); let inner = SubstituteName::one(state, context, binder.name, unification, inner)?; - subtype_with_mode(state, context, mode, inner, t2) + subtype_with::(state, context, inner, t2) } - (Type::Constrained(constraint, inner), _) if mode == ElaborationMode::Yes => { - state.push_wanted(constraint); - subtype_with_mode(state, context, mode, inner, t2) + (Type::Constrained(constraint, constrained), _) => { + P::on_constrained(state, context, t1, constraint, constrained, t2) } ( @@ -109,7 +155,7 @@ where if let (Type::Row(t1_row_id), Type::Row(t2_row_id)) = (t1_argument_core, t2_argument_core) { - subtype_rows(state, context, mode, t1_row_id, t2_row_id) + subtype_rows::(state, context, t1_row_id, t2_row_id) } else { unify(state, context, t1, t2) } @@ -493,14 +539,14 @@ where unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) } -fn subtype_rows( +fn subtype_rows( state: &mut CheckState, context: &CheckContext, - mode: ElaborationMode, t1: RowTypeId, t2: RowTypeId, ) -> QueryResult where + P: SubtypePolicy, Q: ExternalQueries, { let t1_row = context.lookup_row_type(t1); @@ -511,7 +557,7 @@ where context, &t1_row, &t2_row, - |state, context, left, right| subtype_with_mode(state, context, mode, left, right), + |state, context, left, right| subtype_with::(state, context, left, right), )?; if !ok { From 74611fb7a921117ea4297314fdd7065f7a762079 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 18:46:22 +0800 Subject: [PATCH 203/386] Update documentation for SubstituteName --- compiler-core/checking2/src/core/substitute.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler-core/checking2/src/core/substitute.rs b/compiler-core/checking2/src/core/substitute.rs index 02e1a735..2d4c494b 100644 --- a/compiler-core/checking2/src/core/substitute.rs +++ b/compiler-core/checking2/src/core/substitute.rs @@ -12,11 +12,11 @@ use crate::state::CheckState; pub type NameToType = FxHashMap; -/// Substitutes rigid type variables identified by [`Name`] with replacement types. +/// Implements [`Name`]-based substitution for [`Type::Rigid`] variables. /// -/// Since names are globally unique, no scope tracking is needed. This subsumes -/// both single-name substitution (for instantiation) and multi-name substitution -/// (for specialising class superclasses with instance arguments). +/// Names are globally unique, removing the need for scope tracking and +/// removing the need for capture-avoiding substitutions. This property +/// is extremely useful for for instantiation. pub struct SubstituteName { bindings: NameToType, } From 1cdea7bfa881eb26f4a264d0f477c86be77733a9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 18:52:20 +0800 Subject: [PATCH 204/386] Add documentation for function subtyping --- compiler-core/checking2/src/core/unification.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 5e82ed64..12f92443 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -107,11 +107,18 @@ where let t2_core = context.queries.lookup_type(t2); match (t1_core, t2_core) { + // Function subtyping is contravariant on the argument type and + // covariant on the result type. It must also be non-elaborating + // on both positions; it's impossible to generate any constraint + // dictionaries at this position. (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { Ok(subtype_with::(state, context, t2_argument, t1_argument)? && subtype_with::(state, context, t1_result, t2_result)?) } + // Normalise Type::Function into Type::Application before unification + // This is explained in intern_function_application, it simplifies the + // subtyping rule tremendously vs. pattern matching Type::Application (Type::Application(_, _), Type::Function(t2_argument, t2_result)) => { let t2 = context.intern_function_application(t2_argument, t2_result); subtype_with::(state, context, t1, t2) From bcd356674d983741461916798280b9c65d79b89d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 20:24:41 +0800 Subject: [PATCH 205/386] Add documentation for forall subtyping --- .../checking2/src/core/unification.rs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 12f92443..19ff41e6 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -123,12 +123,32 @@ where let t2 = context.intern_function_application(t2_argument, t2_result); subtype_with::(state, context, t1, t2) } - (Type::Function(t1_argument, t1_result), Type::Application(_, _)) => { let t1 = context.intern_function_application(t1_argument, t1_result); subtype_with::(state, context, t1, t2) } + // Forall-R + // + // A <: forall b. B + // A <: B[b := b'] + // + // In practice, this disallows the subtyping + // + // (Int -> Int) <: (forall b. b -> b) + // (Int -> Int) <: (b' -> b') + // + // The rigid variable b' is scoped within the `forall`; this is + // enforced through unification and rigid variables carrying + // their depth, and promote_type checking that unification + // variables cannot be shallower than rigid variable depths. + // + // The classic example is `runST` + // + // runST :: forall a. (forall h. ST h a) -> a + // + // If `?a` is solved to a type containing `h`, the skolem escape + // check is triggered because `h` was defined in a deeper scope. (_, Type::Forall(binder_id, inner)) => { let binder = context.lookup_forall_binder(binder_id); let skolem = state.fresh_rigid(context.queries, binder.kind); @@ -137,6 +157,22 @@ where state.with_depth(|state| subtype_with::(state, context, t1, inner)) } + // Forall-L + // + // forall a. A <: B + // A[a := ?a] <: B + // + // In practice, this allows the subtyping + // + // (forall a. a -> a) <: Int -> Int + // (?a -> ?a) <: Int -> Int + // + // with the substitution [?a := Int], and + // + // (forall a. a -> a) <: (forall b. b -> b) + // (?a -> ?a) <: (b' -> b') + // + // with the substitution [?a := b']. (Type::Forall(binder_id, inner), _) => { let binder = context.lookup_forall_binder(binder_id); let unification = state.fresh_unification(context.queries, binder.kind); From ecacc2e047ce1ebf94043c14d345c1d23e345187 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 21:43:56 +0800 Subject: [PATCH 206/386] Add documentation for record subtyping --- .../checking2/src/core/unification.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 19ff41e6..c00e0673 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -185,6 +185,9 @@ where P::on_constrained(state, context, t1, constraint, constrained, t2) } + // Record subtyping is implemented through subtype_rows. Unlike + // unification, the directionality of subtyping allows us to emit + // errors like AdditionalProperty and PropertyIsMissing. ( Type::Application(t1_function, t1_argument), Type::Application(t2_function, t2_argument), @@ -582,6 +585,22 @@ where unify_row_tails(state, context, t1_row.tail, t2_row.tail, left_only, right_only) } +/// Like [`unify_rows`], but uses [`subtype`] for partitioning. +/// +/// Additionally, the directionality of the [`subtype`] relation enables the +/// addition of [`PropertyIsMissing`] and [`AdditionalProperty`] error when +/// dealing with closed rows. +/// +/// Algorithmically, `t1 <: t2` checks shared labels field-by-field via [`subtype`]. +/// +/// Extra labels are only rejected when they appear against a closed row: +/// - if `t1` is closed, labels exclusive to `t2` are [`PropertyIsMissing`]; +/// - if `t2` is closed, labels exclusive to `t1` are [`AdditionalProperty`]. +/// +/// Open rows permit these extra labels. +/// +/// [`PropertyIsMissing`]: ErrorKind::PropertyIsMissing +/// [`AdditionalProperty`]: ErrorKind::AdditionalProperty fn subtype_rows( state: &mut CheckState, context: &CheckContext, From 066c4457dd5ab16c6fca623da7fbf067439ac0c2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 19 Feb 2026 22:39:10 +0800 Subject: [PATCH 207/386] Add documentation for unification rules --- .../checking2/src/core/unification.rs | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index c00e0673..1fe8d32f 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -231,7 +231,9 @@ where let t2_core = context.queries.lookup_type(t2); let unifies = match (t1_core, t2_core) { - // TODO: document impredicativity + // PureScript has an impredicative type system i.e. unification + // variables can be solved to Type::Forall. These rules must + // be placed before the one-sided Type::Forall skolemisation. (Type::Unification(id), _) => return solve(state, context, t1, id, t2), (_, Type::Unification(id)) => return solve(state, context, t2, id, t1), @@ -258,6 +260,15 @@ where unify(state, context, t1_left, t2_left)? && unify(state, context, t1_right, t2_right)? } + // Forall-Forall + // + // forall a. A ~ forall b. B + // A[a := c] ~ B[b := c] + // + // The rigid variable c is scoped within both `forall`. Unlike + // subtyping that has directionality, unification needs both + // types to be instantiated with the same fresh varible. It + // does not make sense for Forall to instantiate either side. (Type::Forall(t1_binder_id, t1_inner), Type::Forall(t2_binder_id, t2_inner)) => { let t1_binder = context.lookup_forall_binder(t1_binder_id); let t2_binder = context.lookup_forall_binder(t2_binder_id); @@ -272,13 +283,26 @@ where state.with_depth(|state| unify(state, context, t1_inner, t2_inner))? } + // Forall-B, A-Forall + // + // The Type::Unification pattern above is an early catch-all that + // enables impredicativity. In practice, this unifies the following: + // + // (forall a. a -> a) ~ (?b -> ?b) + // (a' -> a') ~ (?b -> ?b) + // + // with the substitution [?b := 'a], and + // + // (?a -> ?a) ~ (forall b. b -> b) + // (?a -> ?a) ~ (b' -> b') + // + // with the substitution [?a := 'b]. (Type::Forall(binder_id, inner), _) => { let binder = context.lookup_forall_binder(binder_id); let skolem = state.fresh_rigid(context.queries, binder.kind); let inner = SubstituteName::one(state, context, binder.name, skolem, inner)?; state.with_depth(|state| unify(state, context, inner, t2))? } - (_, Type::Forall(binder_id, inner)) => { let binder = context.lookup_forall_binder(binder_id); let skolem = state.fresh_rigid(context.queries, binder.kind); @@ -338,7 +362,6 @@ where if !unifies { let t1 = state.pretty_id(context, t1); let t2 = state.pretty_id(context, t2); - state.insert_error(ErrorKind::CannotUnify { t1, t2 }); } @@ -363,7 +386,6 @@ where PromoteResult::OccursCheck | PromoteResult::SkolemEscape => { let t1 = state.pretty_id(context, unification); let t2 = state.pretty_id(context, solution); - state.insert_error(ErrorKind::CannotUnify { t1, t2 }); return Ok(false); } @@ -552,6 +574,13 @@ where check(&mut promote, state, context, solution) } +/// Unification on row types. +/// +/// Algorithmically, `t1 ~ t2` checks shared labels field-by-field via [`unify`]. +/// - if both are closed, then additional fields are disallowed +/// - if `t1` is closed, additional fields in `t2` are disallowed +/// - if `t2` is closed, additional fields in `t1` are disallowed +/// - finally, [`unify_row_tails`] handles the remaining cases fn unify_rows( state: &mut CheckState, context: &CheckContext, @@ -596,8 +625,7 @@ where /// Extra labels are only rejected when they appear against a closed row: /// - if `t1` is closed, labels exclusive to `t2` are [`PropertyIsMissing`]; /// - if `t2` is closed, labels exclusive to `t1` are [`AdditionalProperty`]. -/// -/// Open rows permit these extra labels. +/// - finally, [`unify_row_tails`] handles the remaining cases /// /// [`PropertyIsMissing`]: ErrorKind::PropertyIsMissing /// [`AdditionalProperty`]: ErrorKind::AdditionalProperty @@ -706,20 +734,33 @@ where match (t1_tail, t2_tail) { (None, None) => Ok(true), + // t1_tail ~ ( ...extras_right ) (Some(t1_tail), None) => { let row = RowType::new(extras_right, None); let row_id = context.intern_row_type(row); - let row_ty = context.intern_row(row_id); - unify(state, context, t1_tail, row_ty) + let row_type = context.intern_row(row_id); + unify(state, context, t1_tail, row_type) } + // ( ...extras_left ) ~ t2_tail (None, Some(t2_tail)) => { let row = RowType::new(extras_left, None); let row_id = context.intern_row_type(row); - let row_ty = context.intern_row(row_id); - unify(state, context, t2_tail, row_ty) + let row_type = context.intern_row(row_id); + unify(state, context, t2_tail, row_type) } + // ( | t1_tail ) ~ ( | t2_tail ) + // + // If no additional fields + // + // t1_tail ~ t2_tail + // + // with additional fields + // + // t1_tail ~ ( ...extras_right | ?tail ) + // t2_tail ~ ( ...extras_left | ?tail ) + // (Some(t1_tail), Some(t2_tail)) => { if extras_left.is_empty() && extras_right.is_empty() { return unify(state, context, t1_tail, t2_tail); @@ -729,14 +770,14 @@ where let left_tail_row = RowType::new(extras_right, tail); let left_tail_row_id = context.intern_row_type(left_tail_row); - let left_tail_ty = context.intern_row(left_tail_row_id); + let left_tail_row_type = context.intern_row(left_tail_row_id); let right_tail_row = RowType::new(extras_left, tail); let right_tail_row_id = context.intern_row_type(right_tail_row); - let right_tail_ty = context.intern_row(right_tail_row_id); + let right_tail_row_type = context.intern_row(right_tail_row_id); - Ok(unify(state, context, t1_tail, left_tail_ty)? - && unify(state, context, t2_tail, right_tail_ty)?) + Ok(unify(state, context, t1_tail, left_tail_row_type)? + && unify(state, context, t2_tail, right_tail_row_type)?) } } } From b630b33358495d7406252d00e6e54289d98b48dc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 03:14:20 +0800 Subject: [PATCH 208/386] Add stub for check_data_signature --- compiler-core/checking2/src/source.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 9ee26335..50a608e3 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -54,7 +54,10 @@ where }; match item { - lowering::TypeItemIr::DataGroup { .. } => todo!(), + lowering::TypeItemIr::DataGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_data_signature(state, context, item_id, *signature)?; + } lowering::TypeItemIr::NewtypeGroup { .. } => todo!(), lowering::TypeItemIr::SynonymGroup { .. } => todo!(), lowering::TypeItemIr::ClassGroup { .. } => todo!(), @@ -68,6 +71,21 @@ where Ok(()) } +fn check_data_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + signature: lowering::TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; + let inferred_type = generalise::generalise(state, context, inferred_type)?; + state.checked.types.insert(item_id, inferred_type); + Ok(()) +} + fn check_foreign_signature( state: &mut CheckState, context: &CheckContext, From 53d98cc4762615d3556e98c3b01adc28ffa5bd8d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 13:46:35 +0800 Subject: [PATCH 209/386] Implement ReplaceThen for folding --- compiler-core/checking2/src/core/fold.rs | 25 +++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/compiler-core/checking2/src/core/fold.rs b/compiler-core/checking2/src/core/fold.rs index 38be8e12..a594e0b9 100644 --- a/compiler-core/checking2/src/core/fold.rs +++ b/compiler-core/checking2/src/core/fold.rs @@ -4,14 +4,15 @@ use std::sync::Arc; use building_types::QueryResult; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::normalise::normalise; use crate::core::{ForallBinder, Type, TypeId}; use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; pub enum FoldAction { Replace(TypeId), + ReplaceThen(TypeId), Continue, } @@ -37,13 +38,27 @@ where Q: ExternalQueries, F: TypeFold, { - let id = normalise(state, context, id)?; - let t = context.lookup_type(id); + let mut id = normalise(state, context, id)?; - if let FoldAction::Replace(id) = folder.transform(state, context, id, &t)? { - return Ok(id); + safe_loop! { + let t = context.lookup_type(id); + match folder.transform(state, context, id, &t)? { + FoldAction::Replace(id) => return Ok(id), + FoldAction::ReplaceThen(then_id) => { + let then_id = normalise(state, context, then_id)?; + if then_id == id { + break; + } + id = then_id; + continue; + } + FoldAction::Continue => { + break; + }, + } } + let t = context.lookup_type(id); let result = match t { Type::Application(function, argument) => { let function = fold_type(state, context, function, folder)?; From 3c19d3035f68c1e81c848feb67d6e3ed10ccbc54 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 03:34:09 +0800 Subject: [PATCH 210/386] Implement generalisation --- Cargo.lock | 1 + compiler-core/checking2/Cargo.toml | 1 + .../checking2/src/core/generalise.rs | 167 +++++++++++++++++- .../checking2/src/core/substitute.rs | 41 +++++ 4 files changed, 205 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b18a47f5..c1244912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,7 @@ dependencies = [ "itertools 0.14.0", "lowering", "parsing", + "petgraph", "pretty", "resolving", "rustc-hash 2.1.1", diff --git a/compiler-core/checking2/Cargo.toml b/compiler-core/checking2/Cargo.toml index 8cb17df1..da9d7892 100644 --- a/compiler-core/checking2/Cargo.toml +++ b/compiler-core/checking2/Cargo.toml @@ -10,6 +10,7 @@ indexing = { version = "0.1.0", path = "../indexing" } interner = { version = "0.1.0", path = "../interner" } itertools = "0.14.0" lowering = { version = "0.1.0", path = "../lowering" } +petgraph = "0.8.3" pretty = "0.12" parsing = { version = "0.1.0", path = "../parsing" } resolving = { version = "0.1.0", path = "../resolving" } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 5bac8cae..0f080c60 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -19,20 +19,177 @@ //! [rigid type variables]: crate::core::Type::Rigid use building_types::QueryResult; +use petgraph::algo; +use petgraph::prelude::DiGraphMap; +use rustc_hash::FxHashSet; +use smol_str::SmolStr; + +use crate::core::normalise; +use crate::core::substitute::{SubstituteUnification, UnificationToType}; +use crate::core::{ForallBinder, Type, TypeId}; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::TypeId; -use crate::state::CheckState; +use crate::state::{CheckState, UnificationEntry}; + +type UniGraph = DiGraphMap; + +fn collect_unification_into( + graph: &mut UniGraph, + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + fn aux( + graph: &mut UniGraph, + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + dependent: Option, + visited_kinds: &mut FxHashSet, + ) -> QueryResult<()> + where + Q: ExternalQueries, + { + let id = normalise::normalise(state, context, id)?; + let t = context.lookup_type(id); + + match t { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + aux(graph, state, context, function, dependent, visited_kinds)?; + aux(graph, state, context, argument, dependent, visited_kinds)?; + } + Type::OperatorApplication(_, _, left, right) => { + aux(graph, state, context, left, dependent, visited_kinds)?; + aux(graph, state, context, right, dependent, visited_kinds)?; + } + Type::SynonymApplication(synonym_id) => { + let synonym = context.lookup_synonym(synonym_id); + for &argument in synonym.arguments.iter() { + aux(graph, state, context, argument, dependent, visited_kinds)?; + } + } + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + aux(graph, state, context, binder.kind, dependent, visited_kinds)?; + aux(graph, state, context, inner, dependent, visited_kinds)?; + } + Type::Constrained(constraint, inner) => { + aux(graph, state, context, constraint, dependent, visited_kinds)?; + aux(graph, state, context, inner, dependent, visited_kinds)?; + } + Type::Function(argument, result) => { + aux(graph, state, context, argument, dependent, visited_kinds)?; + aux(graph, state, context, result, dependent, visited_kinds)?; + } + Type::Kinded(inner, kind) => { + aux(graph, state, context, inner, dependent, visited_kinds)?; + aux(graph, state, context, kind, dependent, visited_kinds)?; + } + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + for field in row.fields.iter() { + aux(graph, state, context, field.id, dependent, visited_kinds)?; + } + if let Some(tail) = row.tail { + aux(graph, state, context, tail, dependent, visited_kinds)?; + } + } + Type::Rigid(_, _, kind) => { + aux(graph, state, context, kind, dependent, visited_kinds)?; + } + Type::Unification(unification_id) => { + graph.add_node(unification_id); + + if let Some(dependent_id) = dependent { + graph.add_edge(dependent_id, unification_id, ()); + } + + if visited_kinds.insert(unification_id) { + let entry = state.unifications.get(unification_id); + aux(graph, state, context, entry.kind, Some(unification_id), visited_kinds)?; + } + } + Type::Constructor(_, _) + | Type::OperatorConstructor(_, _) + | Type::Integer(_) + | Type::String(_, _) + | Type::Free(_) + | Type::Unknown(_) => {} + } + + Ok(()) + } + + let mut visited_kinds = FxHashSet::default(); + aux(graph, state, context, id, None, &mut visited_kinds) +} + +fn generate_type_name(id: u32) -> SmolStr { + SmolStr::new(format!("t{id}")) +} /// Generalises a given type. See also module-level documentation. pub fn generalise( - _state: &mut CheckState, - _context: &CheckContext, + state: &mut CheckState, + context: &CheckContext, id: TypeId, ) -> QueryResult where Q: ExternalQueries, { - Ok(id) + let mut graph = UniGraph::new(); + collect_unification_into(&mut graph, state, context, id)?; + + if graph.node_count() == 0 { + return Ok(id); + } + + let Ok(unsolved) = algo::toposort(&graph, None) else { + return Ok(id); + }; + + let mut quantified = id; + let mut substitutions = UnificationToType::default(); + + // All rigid type variables in a single generalisation share the same + // depth, one level deeper than the ambient scope. Note that the depth + // refers to the nesting level of forall scopes with respect to higher + // rank types, not the number of bindings introduced. For example, + // + // forall a b. a -> b -> a + // + // has `a` and `b` on the same depth, whereas, + // + // forall a. (forall r. ST r a) -> a + // forall a. a -> (forall b. b -> a) + // + // have `r` and `b` one level deeper. Note that the latter example is + // actually still a Rank-1 type; a forall can be floated trivially when + // it occurs to the right of the function arrow. + // + // forall a. a -> (forall b. b -> a) + // forall a b. a -> b -> a + // + // See also: https://wiki.haskell.org/Rank-N_types + let depth = state.depth.increment(); + + for &unification_id in unsolved.iter() { + let UnificationEntry { kind, .. } = *state.unifications.get(unification_id); + + let text = generate_type_name(unification_id); + let name = state.names.fresh(); + + let binder = ForallBinder { visible: false, name, text, kind }; + let binder = context.intern_forall_binder(binder); + quantified = context.intern_forall(binder, quantified); + + let rigid = context.intern_rigid(name, depth, kind); + substitutions.insert(unification_id, rigid); + } + + SubstituteUnification::on(state, context, &substitutions, quantified) } diff --git a/compiler-core/checking2/src/core/substitute.rs b/compiler-core/checking2/src/core/substitute.rs index 2d4c494b..684a0b99 100644 --- a/compiler-core/checking2/src/core/substitute.rs +++ b/compiler-core/checking2/src/core/substitute.rs @@ -11,6 +11,7 @@ use crate::core::{Name, Type, TypeId}; use crate::state::CheckState; pub type NameToType = FxHashMap; +pub type UnificationToType = FxHashMap; /// Implements [`Name`]-based substitution for [`Type::Rigid`] variables. /// @@ -69,3 +70,43 @@ impl TypeFold for SubstituteName { } } } + +/// Implements substitution for [`Type::Unification`] variables. +pub struct SubstituteUnification<'a> { + substitutions: &'a UnificationToType, +} + +impl<'a> SubstituteUnification<'a> { + pub fn on( + state: &mut CheckState, + context: &CheckContext, + substitutions: &'a UnificationToType, + in_type: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + fold_type(state, context, in_type, &mut SubstituteUnification { substitutions }) + } +} + +impl TypeFold for SubstituteUnification<'_> { + fn transform( + &mut self, + _state: &mut CheckState, + _context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Unification(unification_id) = t + && let Some(replacement) = self.substitutions.get(unification_id) + { + Ok(FoldAction::ReplaceThen(*replacement)) + } else { + Ok(FoldAction::Continue) + } + } +} From 2343c13e75fb0192985296a9d437405467805203 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 20:14:14 +0800 Subject: [PATCH 211/386] Implement zonking traversal --- compiler-core/checking2/src/core.rs | 1 + .../checking2/src/core/generalise.rs | 3 +- compiler-core/checking2/src/core/zonk.rs | 37 +++++++++++++++++++ compiler-core/checking2/src/source.rs | 1 + 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 compiler-core/checking2/src/core/zonk.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index d5f34a2e..ce8a0b9c 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -7,6 +7,7 @@ pub mod pretty; pub mod substitute; pub mod unification; pub mod walk; +pub mod zonk; use std::sync::Arc; diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 0f080c60..96bfb7c2 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -24,9 +24,8 @@ use petgraph::prelude::DiGraphMap; use rustc_hash::FxHashSet; use smol_str::SmolStr; -use crate::core::normalise; use crate::core::substitute::{SubstituteUnification, UnificationToType}; -use crate::core::{ForallBinder, Type, TypeId}; +use crate::core::{ForallBinder, Type, TypeId, normalise}; use crate::ExternalQueries; use crate::context::CheckContext; diff --git a/compiler-core/checking2/src/core/zonk.rs b/compiler-core/checking2/src/core/zonk.rs new file mode 100644 index 00000000..a5a1c266 --- /dev/null +++ b/compiler-core/checking2/src/core/zonk.rs @@ -0,0 +1,37 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::fold::{FoldAction, TypeFold, fold_type}; +use crate::core::{Type, TypeId}; +use crate::state::CheckState; + +pub struct Zonk; + +impl TypeFold for Zonk { + fn transform( + &mut self, + _state: &mut CheckState, + _context: &CheckContext, + _id: TypeId, + _t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + Ok(FoldAction::Continue) + } +} + +impl Zonk { + pub fn on( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + fold_type(state, context, id, &mut Zonk) + } +} diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 50a608e3..2d2916ff 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -11,6 +11,7 @@ use lowering::Scc; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::generalise; +use crate::core::zonk::Zonk; use crate::state::CheckState; /// Checks all type items in topological order. From e7558a862a8253b1114c2c8837b7b1a9c11a6f70 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 20:51:19 +0800 Subject: [PATCH 212/386] Implement stateless pretty printer --- compiler-core/checking2/src/core/pretty.rs | 92 ++++++++----------- .../checking2/src/core/unification.rs | 8 +- compiler-core/checking2/src/source.rs | 1 - compiler-core/checking2/src/source/synonym.rs | 12 ++- compiler-core/checking2/src/source/types.rs | 8 +- compiler-core/checking2/src/state.rs | 16 ++-- 6 files changed, 61 insertions(+), 76 deletions(-) diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 20880f1f..170036b6 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -7,12 +7,10 @@ use rustc_hash::FxHashMap; use smol_str::{SmolStr, SmolStrBuilder}; use crate::ExternalQueries; -use crate::context::CheckContext; use crate::core::{ ForallBinder, ForallBinderId, Name, RowField, RowType, RowTypeId, SmolStrId, Synonym, SynonymId, Type, TypeId, }; -use crate::state::CheckState; type Doc<'a> = DocBuilder<'a, Arena<'a>, ()>; @@ -26,42 +24,29 @@ impl Default for PrettyConfig { } } -pub fn print(state: &mut CheckState, context: &CheckContext, id: TypeId) -> SmolStr +pub fn print(queries: &Q, id: TypeId) -> SmolStr where Q: ExternalQueries, { - render(state, context, &PrettyConfig::default(), |printer| { - printer.traverse(Precedence::Top, id) - }) + render(queries, &PrettyConfig::default(), |printer| printer.traverse(Precedence::Top, id)) } -pub fn print_with_config( - state: &mut CheckState, - context: &CheckContext, - id: TypeId, - config: &PrettyConfig, -) -> SmolStr +pub fn print_with_config(queries: &Q, id: TypeId, config: &PrettyConfig) -> SmolStr where Q: ExternalQueries, { - render(state, context, config, |printer| printer.traverse(Precedence::Top, id)) + render(queries, config, |printer| printer.traverse(Precedence::Top, id)) } -pub fn print_signature( - state: &mut CheckState, - context: &CheckContext, - name: &str, - id: TypeId, -) -> SmolStr +pub fn print_signature(queries: &Q, name: &str, id: TypeId) -> SmolStr where Q: ExternalQueries, { - render(state, context, &PrettyConfig::default(), |printer| printer.signature(name, id)) + render(queries, &PrettyConfig::default(), |printer| printer.signature(name, id)) } pub fn print_signature_with_config( - state: &mut CheckState, - context: &CheckContext, + queries: &Q, name: &str, id: TypeId, config: &PrettyConfig, @@ -69,21 +54,20 @@ pub fn print_signature_with_config( where Q: ExternalQueries, { - render(state, context, config, |printer| printer.signature(name, id)) + render(queries, config, |printer| printer.signature(name, id)) } fn render( - state: &mut CheckState, - context: &CheckContext, + queries: &Q, config: &PrettyConfig, - f: impl for<'a> FnOnce(&mut Printer<'a, '_, Q>) -> Doc<'a>, + f: impl for<'a> FnOnce(&mut Printer<'a, Q>) -> Doc<'a>, ) -> SmolStr where Q: ExternalQueries, { let arena = Arena::new(); let traversal = TraversalContext::new(); - let mut printer = Printer::new(&arena, state, context, traversal); + let mut printer = Printer::new(&arena, queries, traversal); let document = f(&mut printer); let mut output = SmolStrBuilder::new(); @@ -124,48 +108,41 @@ impl TraversalContext { } } -struct Printer<'a, 'q, Q> +struct Printer<'a, Q> where Q: ExternalQueries, { arena: &'a Arena<'a>, - state: &'a mut CheckState, - context: &'a CheckContext<'q, Q>, + queries: &'a Q, traversal: TraversalContext, } -impl<'a, 'q, Q> Printer<'a, 'q, Q> +impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { - fn new( - arena: &'a Arena<'a>, - state: &'a mut CheckState, - context: &'a CheckContext<'q, Q>, - traversal: TraversalContext, - ) -> Printer<'a, 'q, Q> { - Printer { arena, state, context, traversal } + fn new(arena: &'a Arena<'a>, queries: &'a Q, traversal: TraversalContext) -> Printer<'a, Q> { + Printer { arena, queries, traversal } } - fn lookup_type(&mut self, id: TypeId) -> Type { - let id = crate::core::normalise::normalise(self.state, self.context, id).unwrap_or(id); - self.context.queries.lookup_type(id) + fn lookup_type(&self, id: TypeId) -> Type { + self.queries.lookup_type(id) } fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder { - self.context.queries.lookup_forall_binder(id) + self.queries.lookup_forall_binder(id) } fn lookup_row_type(&self, id: RowTypeId) -> RowType { - self.context.queries.lookup_row_type(id) + self.queries.lookup_row_type(id) } fn lookup_synonym(&self, id: SynonymId) -> Synonym { - self.context.queries.lookup_synonym(id) + self.queries.lookup_synonym(id) } fn lookup_smol_str(&self, id: SmolStrId) -> smol_str::SmolStr { - self.context.queries.lookup_smol_str(id) + self.queries.lookup_smol_str(id) } fn lookup_type_name( @@ -173,10 +150,20 @@ where file_id: files::FileId, type_id: indexing::TypeItemId, ) -> Option { - let indexed = self.context.queries.indexed(file_id).ok()?; + let indexed = self.queries.indexed(file_id).ok()?; indexed.items[type_id].name.as_ref().map(|name| name.to_string()) } + fn is_record_constructor(&self, id: TypeId) -> bool { + if let Type::Constructor(file_id, type_id) = self.lookup_type(id) + && file_id == self.queries.prim_id() + && let Some(name) = self.lookup_type_name(file_id, type_id) + { + return name == "Record"; + } + false + } + fn parens_if(&self, condition: bool, doc: Doc<'a>) -> Doc<'a> { if condition { self.arena.text("(").append(doc).append(self.arena.text(")")) } else { doc } } @@ -296,10 +283,7 @@ where self.arena.text(format!("({name} :: ")).append(kind).append(self.arena.text(")")) } - Type::Unification(unification_id) => { - let entry = self.state.unifications.get(unification_id); - self.arena.text(format!("?{unification_id}[{}]", entry.depth.0)) - } + Type::Unification(unification_id) => self.arena.text(format!("?{unification_id}")), Type::Free(name_id) => { let name = self.lookup_smol_str(name_id); @@ -314,7 +298,7 @@ where } } -impl<'a, 'q, Q> Printer<'a, 'q, Q> +impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { @@ -324,7 +308,7 @@ where mut function: TypeId, argument: TypeId, ) -> Doc<'a> { - if function == self.context.prim.record { + if self.is_record_constructor(function) { return self.format_record_application(argument); } @@ -394,7 +378,7 @@ where } } -impl<'a, 'q, Q> Printer<'a, 'q, Q> +impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { @@ -506,7 +490,7 @@ where } } -impl<'a, 'q, Q> Printer<'a, 'q, Q> +impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 1fe8d32f..766fec92 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -360,8 +360,8 @@ where }; if !unifies { - let t1 = state.pretty_id(context, t1); - let t2 = state.pretty_id(context, t2); + let t1 = state.pretty_id(context, t1)?; + let t2 = state.pretty_id(context, t2)?; state.insert_error(ErrorKind::CannotUnify { t1, t2 }); } @@ -384,8 +384,8 @@ where match promote_type(state, context, id, solution)? { PromoteResult::Ok => {} PromoteResult::OccursCheck | PromoteResult::SkolemEscape => { - let t1 = state.pretty_id(context, unification); - let t2 = state.pretty_id(context, solution); + let t1 = state.pretty_id(context, unification)?; + let t2 = state.pretty_id(context, solution)?; state.insert_error(ErrorKind::CannotUnify { t1, t2 }); return Ok(false); } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 2d2916ff..50a608e3 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -11,7 +11,6 @@ use lowering::Scc; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::generalise; -use crate::core::zonk::Zonk; use crate::state::CheckState; /// Checks all type items in topological order. diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 43b70887..24a206e0 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -203,9 +203,11 @@ where let (argument_type, _) = types::infer_kind(state, context, argument)?; { - let function_type = state.pretty_id(context, context.unknown("synonym function")); - let function_kind = state.pretty_id(context, function_kind); - let argument_type = state.pretty_id(context, argument_type); + let function_type = context.unknown("unknown function type"); + let function_type = state.pretty_id(context, function_type)?; + + let function_kind = state.pretty_id(context, function_kind)?; + let argument_type = state.pretty_id(context, argument_type)?; state.insert_error(ErrorKind::InvalidTypeApplication { function_type, @@ -214,8 +216,8 @@ where }); } - let unknown = context.unknown("synonym function kind cannot be applied"); - Ok((argument_type, unknown)) + let k = context.unknown("cannot apply synonym"); + Ok((argument_type, k)) } } } diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 7c94d0af..ee57cf41 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -445,11 +445,11 @@ where let (argument_type, _) = infer_kind(state, context, argument)?; let t = context.intern_application(function_type, argument_type); - let k = context.unknown("function type cannot be applied"); + let k = context.unknown("cannot apply function type"); - let function_type = state.pretty_id(context, function_type); - let function_kind = state.pretty_id(context, function_kind); - let argument_type = state.pretty_id(context, argument_type); + let function_type = state.pretty_id(context, function_type)?; + let function_kind = state.pretty_id(context, function_kind)?; + let argument_type = state.pretty_id(context, argument_type)?; state.insert_error(ErrorKind::InvalidTypeApplication { function_type, diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 2a71474f..9894946a 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -2,10 +2,13 @@ use std::mem; +use building_types::QueryResult; use files::FileId; use rustc_hash::FxHashMap; -use crate::core::{Depth, Name, SmolStrId, Type, TypeId}; +use crate::context::CheckContext; +use crate::core::zonk::Zonk; +use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; @@ -189,15 +192,12 @@ impl CheckState { result } - pub fn pretty_id( - &mut self, - context: &crate::context::CheckContext, - id: TypeId, - ) -> SmolStrId + pub fn pretty_id(&mut self, context: &CheckContext, id: TypeId) -> QueryResult where Q: ExternalQueries, { - let pretty = crate::core::pretty::print(self, context, id); - context.queries.intern_smol_str(pretty) + let id = Zonk::on(self, context, id)?; + let pretty = pretty::print(context.queries, id); + Ok(context.queries.intern_smol_str(pretty)) } } From 165ebeab6bca0568bb6633992fa3afb34c36ff22 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 21 Feb 2026 01:37:28 +0800 Subject: [PATCH 213/386] Align synonym application with type application --- compiler-core/checking2/src/source/synonym.rs | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 24a206e0..9d80f1a5 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -92,7 +92,7 @@ pub fn infer_synonym_application( state: &mut CheckState, context: &CheckContext, id: lowering::TypeId, - (file_id, type_id, synonym, kind): (FileId, TypeItemId, Synonym, TypeId), + (file_id, type_id, synonym, function_kind): (FileId, TypeItemId, Synonym, TypeId), arguments: &[lowering::TypeId], ) -> QueryResult<(TypeId, TypeId)> where @@ -113,8 +113,13 @@ where let (synonym_arguments, excess_arguments) = arguments.split_at(expected_arity); - let (argument_types, synonym_kind) = - infer_synonym_application_arguments(state, context, kind, synonym_arguments)?; + let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let (argument_types, (_, synonym_kind)) = infer_synonym_application_chain( + state, + context, + (function_type, function_kind), + synonym_arguments, + )?; let synonym = Synonym { saturation: Saturation::Full, @@ -134,35 +139,35 @@ where Ok((synonym_type, synonym_kind)) } -fn infer_synonym_application_arguments( +fn infer_synonym_application_chain( state: &mut CheckState, context: &CheckContext, - kind: TypeId, + function: (TypeId, TypeId), arguments: &[lowering::TypeId], -) -> QueryResult<(Vec, TypeId)> +) -> QueryResult<(Vec, (TypeId, TypeId))> where Q: ExternalQueries, { let mut argument_types = vec![]; - let mut synonym_kind = kind; + let mut current_function = function; for &argument_id in arguments { - let (argument_type, result_kind) = - infer_synonym_application_argument(state, context, synonym_kind, argument_id)?; + let (argument_type, result_function) = + infer_synonym_application_kind(state, context, current_function, argument_id)?; argument_types.push(argument_type); - synonym_kind = result_kind; + current_function = result_function; } - Ok((argument_types, synonym_kind)) + Ok((argument_types, current_function)) } -fn infer_synonym_application_argument( +fn infer_synonym_application_kind( state: &mut CheckState, context: &CheckContext, - function_kind: TypeId, + (function_type, function_kind): (TypeId, TypeId), argument: lowering::TypeId, -) -> QueryResult<(TypeId, TypeId)> +) -> QueryResult<(TypeId, (TypeId, TypeId))> where Q: ExternalQueries, { @@ -172,7 +177,8 @@ where Type::Function(argument_kind, result_kind) => { let (argument_type, _) = types::check_kind(state, context, argument, argument_kind)?; let result_kind = normalise::normalise(state, context, result_kind)?; - Ok((argument_type, result_kind)) + let result_type = context.intern_application(function_type, argument_type); + Ok((argument_type, (result_type, result_kind))) } Type::Unification(unification_id) => { @@ -184,8 +190,9 @@ where let (argument_type, _) = types::check_kind(state, context, argument, argument_u)?; let result_kind = normalise::normalise(state, context, result_u)?; + let result_type = context.intern_application(function_type, argument_type); - Ok((argument_type, result_kind)) + Ok((argument_type, (result_type, result_kind))) } Type::Forall(binder_id, inner_kind) => { @@ -193,19 +200,21 @@ where let binder_kind = normalise::normalise(state, context, binder.kind)?; let kind_argument = state.fresh_unification(context.queries, binder_kind); + let function_type = context.intern_kind_application(function_type, kind_argument); let function_kind = SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; - infer_synonym_application_argument(state, context, function_kind, argument) + infer_synonym_application_kind(state, context, (function_type, function_kind), argument) } _ => { let (argument_type, _) = types::infer_kind(state, context, argument)?; + let t = context.intern_application(function_type, argument_type); + let k = context.unknown("cannot apply synonym type"); + { - let function_type = context.unknown("unknown function type"); let function_type = state.pretty_id(context, function_type)?; - let function_kind = state.pretty_id(context, function_kind)?; let argument_type = state.pretty_id(context, argument_type)?; @@ -216,8 +225,7 @@ where }); } - let k = context.unknown("cannot apply synonym"); - Ok((argument_type, k)) + Ok((argument_type, (t, k))) } } } From 937d496d9ef8d92004a58a007d62b6561d7bec05 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 20 Feb 2026 23:38:52 +0800 Subject: [PATCH 214/386] Add as_slice and is_recursive for Scc --- compiler-core/lowering/src/lib.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/compiler-core/lowering/src/lib.rs b/compiler-core/lowering/src/lib.rs index 190ab229..841b722e 100644 --- a/compiler-core/lowering/src/lib.rs +++ b/compiler-core/lowering/src/lib.rs @@ -8,8 +8,8 @@ pub mod intermediate; pub mod scope; pub mod source; -use std::hash::Hash; use std::sync::Arc; +use std::{hash::Hash, slice}; pub use error::*; pub use intermediate::*; @@ -54,6 +54,19 @@ pub enum Scc { Mutual(Vec), } +impl Scc { + pub fn as_slice(&self) -> &[T] { + match self { + Scc::Base(item) | Scc::Recursive(item) => slice::from_ref(item), + Scc::Mutual(items) => items.as_slice(), + } + } + + pub fn is_recursive(&self) -> bool { + matches!(self, Scc::Recursive(..) | Scc::Mutual(..)) + } +} + pub fn lower_module( file_id: FileId, module: &cst::Module, From 856a404d42c5890089ba916c58ef6f6e4e358995 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 21 Feb 2026 00:29:13 +0800 Subject: [PATCH 215/386] Implement binding group seeding for SCCs --- compiler-core/checking2/src/source.rs | 112 +++++++++++++++++++++----- 1 file changed, 93 insertions(+), 19 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 50a608e3..49541d16 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -6,11 +6,10 @@ pub mod types; use building_types::QueryResult; use indexing::TypeItemId; -use lowering::Scc; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::generalise; +use crate::core::{generalise, zonk}; use crate::state::CheckState; /// Checks all type items in topological order. @@ -19,28 +18,103 @@ where Q: ExternalQueries, { for scc in &context.grouped.type_scc { - match scc { - Scc::Base(id) => { - check_type_signature(state, context, *id)?; - check_type_equation(state, context, *id)?; - } - Scc::Recursive(id) => { - check_type_signature(state, context, *id)?; - check_type_equation(state, context, *id)?; - } - Scc::Mutual(mutual) => { - for id in mutual { - check_type_signature(state, context, *id)?; - } - for &id in mutual { - check_type_equation(state, context, id)?; - } - } + let items = filter_type_items(context, scc); + + for &item in &items { + check_type_signature(state, context, item)?; + } + + if scc.is_recursive() { + prepare_binding_group(state, context, &items); } + + for &item in &items { + check_type_equation(state, context, item)?; + } + + finalise_binding_group(state, context, &items)?; } Ok(()) } +fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TypeItemId]) +where + Q: ExternalQueries, +{ + for &item_id in items { + if state.checked.types.contains_key(&item_id) { + continue; + } + let kind = state.fresh_unification(context.queries, context.prim.t); + state.checked.types.insert(item_id, kind); + } +} + +fn finalise_binding_group( + state: &mut CheckState, + context: &CheckContext, + items: &[TypeItemId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &item_id in items { + let Some(kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let kind = zonk::Zonk::on(state, context, kind)?; + let kind = generalise::generalise(state, context, kind)?; + state.checked.types.insert(item_id, kind); + } + + Ok(()) +} + +fn filter_type_items( + context: &CheckContext, + scc: &lowering::Scc, +) -> Vec +where + Q: ExternalQueries, +{ + scc.as_slice() + .iter() + .copied() + .filter(|&item_id| { + if has_recursive_kind_error(context, item_id) { + return false; + } + + // Recursive groups should only contain proper equations. + !(scc.is_recursive() && is_foreign_item(context, item_id)) + }) + .collect() +} + +fn has_recursive_kind_error(context: &CheckContext, item_id: TypeItemId) -> bool +where + Q: ExternalQueries, +{ + context.grouped.cycle_errors.iter().any(|error| { + if let lowering::LoweringError::RecursiveKinds(recursive) = error { + recursive.group.contains(&item_id) + } else { + false + } + }) +} + +fn is_foreign_item(context: &CheckContext, item_id: TypeItemId) -> bool +where + Q: ExternalQueries, +{ + matches!( + context.lowered.info.get_type_item(item_id), + Some(lowering::TypeItemIr::Foreign { .. }) + ) +} + fn check_type_signature( state: &mut CheckState, context: &CheckContext, From f479eeec71bf9057c9616eb58ebf35c7ccda4fce Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 21 Feb 2026 02:47:04 +0800 Subject: [PATCH 216/386] Implement core::toolkit and function_components --- compiler-core/checking2/src/core.rs | 1 + compiler-core/checking2/src/core/toolkit.rs | 51 +++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 compiler-core/checking2/src/core/toolkit.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index ce8a0b9c..498ea1f7 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -5,6 +5,7 @@ pub mod generalise; pub mod normalise; pub mod pretty; pub mod substitute; +pub mod toolkit; pub mod unification; pub mod walk; pub mod zonk; diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs new file mode 100644 index 00000000..f443cf96 --- /dev/null +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -0,0 +1,51 @@ +//! Implements shared utilities for core type operations. + +use building_types::QueryResult; + +use crate::context::CheckContext; +use crate::core::{Type, TypeId, normalise}; +use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; + +/// Splits a function-like type into argument kinds and a result kind. +pub fn function_components( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult<(Vec, TypeId)> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + let mut current = id; + + safe_loop! { + current = normalise::normalise(state, context, current)?; + + match context.lookup_type(current) { + Type::Forall(_, inner) => { + current = inner; + } + Type::Function(argument, result) => { + arguments.push(argument); + current = result; + } + Type::Application(function_argument, result) => { + let function_argument = normalise::normalise(state, context, function_argument)?; + let Type::Application(function, argument) = context.lookup_type(function_argument) else { + break; + }; + let function = normalise::normalise(state, context, function)?; + if function == context.prim.function { + arguments.push(argument); + current = result; + } else { + break; + } + } + _ => break, + } + } + + Ok((arguments, current)) +} From dc6a916abbfdb9a2ba5d13a427e3e21762556b95 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 21 Feb 2026 00:30:15 +0800 Subject: [PATCH 217/386] Implement initial check_data_equation --- compiler-core/checking2/src/source.rs | 171 ++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 7 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 49541d16..f86a1fd9 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -6,10 +6,12 @@ pub mod types; use building_types::QueryResult; use indexing::TypeItemId; +use lowering::{DataIr, NewtypeIr}; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{generalise, zonk}; +use crate::core::{TypeId, generalise, toolkit, unification, zonk}; +use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; /// Checks all type items in topological order. @@ -132,7 +134,10 @@ where let Some(signature) = signature else { return Ok(()) }; check_data_signature(state, context, item_id, *signature)?; } - lowering::TypeItemIr::NewtypeGroup { .. } => todo!(), + lowering::TypeItemIr::NewtypeGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_data_signature(state, context, item_id, *signature)?; + } lowering::TypeItemIr::SynonymGroup { .. } => todo!(), lowering::TypeItemIr::ClassGroup { .. } => todo!(), lowering::TypeItemIr::Foreign { signature, .. } => { @@ -155,7 +160,6 @@ where Q: ExternalQueries, { let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; - let inferred_type = generalise::generalise(state, context, inferred_type)?; state.checked.types.insert(item_id, inferred_type); Ok(()) } @@ -170,18 +174,171 @@ where Q: ExternalQueries, { let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; - let inferred_type = generalise::generalise(state, context, inferred_type)?; state.checked.types.insert(item_id, inferred_type); Ok(()) } fn check_type_equation( - _state: &mut CheckState, - _context: &CheckContext, - _item_id: TypeItemId, + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_type_item(item_id) else { + return Ok(()); + }; + + match item { + lowering::TypeItemIr::DataGroup { signature, data, .. } => { + let Some(DataIr { variables }) = data else { return Ok(()) }; + check_data_equation(state, context, item_id, *signature, &variables)?; + } + lowering::TypeItemIr::NewtypeGroup { signature, newtype, .. } => { + let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; + check_data_equation(state, context, item_id, *signature, &variables)?; + } + lowering::TypeItemIr::SynonymGroup { .. } => todo!(), + lowering::TypeItemIr::ClassGroup { .. } => todo!(), + lowering::TypeItemIr::Foreign { .. } => {} + lowering::TypeItemIr::Operator { .. } => todo!(), + } + + Ok(()) +} + +fn check_data_equation( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + signature: Option, + variables: &[lowering::TypeVariableBinding], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + if let Some(signature_id) = signature { + let (matched_arguments, result) = + check_data_equation_variables(state, context, signature_id, item_id, variables)?; + let _ = build_data_equation_kind(state, context, variables, &matched_arguments, result)?; + } else { + let inferred_kind = + build_data_equation_kind(state, context, variables, &[], context.prim.t)?; + + if let Some(known_kind) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred_kind, known_kind)?; + } else { + state.checked.types.insert(item_id, inferred_kind); + } + } + + check_data_constructor_arguments(state, context, item_id)?; + + Ok(()) +} + +fn build_data_equation_kind( + state: &mut CheckState, + context: &CheckContext, + variables: &[lowering::TypeVariableBinding], + matched_arguments: &[TypeId], + result: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut variable_kinds = Vec::with_capacity(variables.len()); + + for (index, binding) in variables.iter().enumerate() { + let expected_kind = matched_arguments.get(index).copied(); + + let kind = if let Some(expected_kind) = expected_kind { + if let Some(kind_id) = binding.kind { + let (kind, _) = types::infer_kind(state, context, kind_id)?; + let valid = unification::subtype(state, context, expected_kind, kind)?; + if valid { kind } else { context.unknown("invalid data equation variable kind") } + } else { + expected_kind + } + } else { + if let Some(kind_id) = binding.kind { + let (kind, _) = types::check_kind(state, context, kind_id, context.prim.t)?; + kind + } else { + state.fresh_unification(context.queries, context.prim.t) + } + }; + + let name = state.names.fresh(); + state.kind_scope.bind_forall(binding.id, name, kind); + variable_kinds.push(kind); + } + + let item_kind = variable_kinds + .iter() + .rfold(result, |result, &argument| context.intern_function(argument, result)); + + Ok(item_kind) +} + +fn check_data_equation_variables( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + item_id: TypeItemId, + variables: &[lowering::TypeVariableBinding], +) -> QueryResult<(Vec, TypeId)> +where + Q: ExternalQueries, +{ + let Some(stored_kind) = state.checked.lookup_type(item_id) else { + return Ok((vec![], context.unknown("missing checked kind for type item"))); + }; + + let (arguments, result) = toolkit::function_components(state, context, stored_kind)?; + + if variables.len() > arguments.len() { + state.insert_error(ErrorKind::TypeSignatureVariableMismatch { + id: signature_id, + expected: arguments.len() as u32, + actual: variables.len() as u32, + }); + } + + let matched_count = variables.len().min(arguments.len()); + let matched_arguments = arguments.iter().take(matched_count).copied().collect::>(); + + let final_kind = arguments + .iter() + .skip(matched_count) + .rfold(result, |result, &argument| context.intern_function(argument, result)); + + Ok((matched_arguments, final_kind)) +} + +fn check_data_constructor_arguments( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, ) -> QueryResult<()> where Q: ExternalQueries, { + for constructor_id in context.indexed.pairs.data_constructors(item_id) { + let Some(lowering::TermItemIr::Constructor { arguments }) = + context.lowered.info.get_term_item(constructor_id) + else { + continue; + }; + + for &argument in arguments.iter() { + state.with_error_crumb(ErrorCrumb::ConstructorArgument(argument), |state| { + let (_, _) = types::check_kind(state, context, argument, context.prim.t)?; + Ok(()) + })?; + } + } + Ok(()) } From e55c8749814c122a546fcac80b52ebf53648e572 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 21 Feb 2026 18:59:52 +0800 Subject: [PATCH 218/386] Partition invalid items and populate with unknowns --- compiler-core/checking2/src/source.rs | 93 ++++++++++++++------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index f86a1fd9..99d26387 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -6,7 +6,10 @@ pub mod types; use building_types::QueryResult; use indexing::TypeItemId; -use lowering::{DataIr, NewtypeIr}; +use lowering::{ + DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, + TypeVariableBinding, +}; use crate::ExternalQueries; use crate::context::CheckContext; @@ -20,7 +23,8 @@ where Q: ExternalQueries, { for scc in &context.grouped.type_scc { - let items = filter_type_items(context, scc); + let (items, skipped) = partition_type_items(context, scc); + populate_skipped_items(state, context, &skipped); for &item in &items { check_type_signature(state, context, item)?; @@ -73,48 +77,49 @@ where Ok(()) } -fn filter_type_items( +fn partition_type_items( context: &CheckContext, - scc: &lowering::Scc, -) -> Vec + scc: &Scc, +) -> (Vec, Vec) where Q: ExternalQueries, { - scc.as_slice() - .iter() - .copied() - .filter(|&item_id| { - if has_recursive_kind_error(context, item_id) { - return false; - } + let mut checked = vec![]; + let mut skipped = vec![]; - // Recursive groups should only contain proper equations. - !(scc.is_recursive() && is_foreign_item(context, item_id)) - }) - .collect() + for &item_id in scc.as_slice() { + if is_recursive_kind(context, item_id) { + skipped.push(item_id); + } else { + checked.push(item_id); + } + } + + (checked, skipped) } -fn has_recursive_kind_error(context: &CheckContext, item_id: TypeItemId) -> bool +fn is_recursive_kind(context: &CheckContext, item_id: TypeItemId) -> bool where Q: ExternalQueries, { context.grouped.cycle_errors.iter().any(|error| { - if let lowering::LoweringError::RecursiveKinds(recursive) = error { - recursive.group.contains(&item_id) - } else { - false - } + let LoweringError::RecursiveKinds(RecursiveGroup { group }) = error else { + return false; + }; + group.contains(&item_id) }) } -fn is_foreign_item(context: &CheckContext, item_id: TypeItemId) -> bool -where +fn populate_skipped_items( + state: &mut CheckState, + context: &CheckContext, + items: &[TypeItemId], +) where Q: ExternalQueries, { - matches!( - context.lowered.info.get_type_item(item_id), - Some(lowering::TypeItemIr::Foreign { .. }) - ) + let unknown = context.unknown("invalid recursive type"); + let skipped = items.iter().map(|item| (*item, unknown)); + state.checked.types.extend(skipped); } fn check_type_signature( @@ -130,21 +135,21 @@ where }; match item { - lowering::TypeItemIr::DataGroup { signature, .. } => { + TypeItemIr::DataGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; check_data_signature(state, context, item_id, *signature)?; } - lowering::TypeItemIr::NewtypeGroup { signature, .. } => { + TypeItemIr::NewtypeGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; check_data_signature(state, context, item_id, *signature)?; } - lowering::TypeItemIr::SynonymGroup { .. } => todo!(), - lowering::TypeItemIr::ClassGroup { .. } => todo!(), - lowering::TypeItemIr::Foreign { signature, .. } => { + TypeItemIr::SynonymGroup { .. } => todo!(), + TypeItemIr::ClassGroup { .. } => todo!(), + TypeItemIr::Foreign { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; check_foreign_signature(state, context, item_id, *signature)?; } - lowering::TypeItemIr::Operator { .. } => todo!(), + TypeItemIr::Operator { .. } => todo!(), } Ok(()) @@ -191,18 +196,18 @@ where }; match item { - lowering::TypeItemIr::DataGroup { signature, data, .. } => { + TypeItemIr::DataGroup { signature, data, .. } => { let Some(DataIr { variables }) = data else { return Ok(()) }; check_data_equation(state, context, item_id, *signature, &variables)?; } - lowering::TypeItemIr::NewtypeGroup { signature, newtype, .. } => { + TypeItemIr::NewtypeGroup { signature, newtype, .. } => { let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; check_data_equation(state, context, item_id, *signature, &variables)?; } - lowering::TypeItemIr::SynonymGroup { .. } => todo!(), - lowering::TypeItemIr::ClassGroup { .. } => todo!(), - lowering::TypeItemIr::Foreign { .. } => {} - lowering::TypeItemIr::Operator { .. } => todo!(), + TypeItemIr::SynonymGroup { .. } => todo!(), + TypeItemIr::ClassGroup { .. } => todo!(), + TypeItemIr::Foreign { .. } => {} + TypeItemIr::Operator { .. } => todo!(), } Ok(()) @@ -213,7 +218,7 @@ fn check_data_equation( context: &CheckContext, item_id: TypeItemId, signature: Option, - variables: &[lowering::TypeVariableBinding], + variables: &[TypeVariableBinding], ) -> QueryResult<()> where Q: ExternalQueries, @@ -241,7 +246,7 @@ where fn build_data_equation_kind( state: &mut CheckState, context: &CheckContext, - variables: &[lowering::TypeVariableBinding], + variables: &[TypeVariableBinding], matched_arguments: &[TypeId], result: TypeId, ) -> QueryResult @@ -287,7 +292,7 @@ fn check_data_equation_variables( context: &CheckContext, signature_id: lowering::TypeId, item_id: TypeItemId, - variables: &[lowering::TypeVariableBinding], + variables: &[TypeVariableBinding], ) -> QueryResult<(Vec, TypeId)> where Q: ExternalQueries, @@ -326,7 +331,7 @@ where Q: ExternalQueries, { for constructor_id in context.indexed.pairs.data_constructors(item_id) { - let Some(lowering::TermItemIr::Constructor { arguments }) = + let Some(TermItemIr::Constructor { arguments }) = context.lowered.info.get_term_item(constructor_id) else { continue; From 3c4e61a0d612dc6acc082b566005adb2397d62ea Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 22 Feb 2026 00:59:27 +0800 Subject: [PATCH 219/386] Move Zonk::on to zonk --- compiler-core/checking2/src/core/zonk.rs | 16 +++++----------- compiler-core/checking2/src/source.rs | 2 +- compiler-core/checking2/src/state.rs | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/compiler-core/checking2/src/core/zonk.rs b/compiler-core/checking2/src/core/zonk.rs index a5a1c266..bef59170 100644 --- a/compiler-core/checking2/src/core/zonk.rs +++ b/compiler-core/checking2/src/core/zonk.rs @@ -23,15 +23,9 @@ impl TypeFold for Zonk { } } -impl Zonk { - pub fn on( - state: &mut CheckState, - context: &CheckContext, - id: TypeId, - ) -> QueryResult - where - Q: ExternalQueries, - { - fold_type(state, context, id, &mut Zonk) - } +pub fn zonk(state: &mut CheckState, context: &CheckContext, id: TypeId) -> QueryResult +where + Q: ExternalQueries, +{ + fold_type(state, context, id, &mut Zonk) } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 99d26387..185fd405 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -69,7 +69,7 @@ where continue; }; - let kind = zonk::Zonk::on(state, context, kind)?; + let kind = zonk::zonk(state, context, kind)?; let kind = generalise::generalise(state, context, kind)?; state.checked.types.insert(item_id, kind); } diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 9894946a..7845bbdd 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -7,7 +7,7 @@ use files::FileId; use rustc_hash::FxHashMap; use crate::context::CheckContext; -use crate::core::zonk::Zonk; +use crate::core::zonk; use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; @@ -196,7 +196,7 @@ impl CheckState { where Q: ExternalQueries, { - let id = Zonk::on(self, context, id)?; + let id = zonk::zonk(self, context, id)?; let pretty = pretty::print(context.queries, id); Ok(context.queries.intern_smol_str(pretty)) } From ec0c141ecc20e4f4253b7fd2fb26eea8a474f3ac Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 22 Feb 2026 01:47:44 +0800 Subject: [PATCH 220/386] Add documentation for check_type_items --- compiler-core/checking2/src/source.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 185fd405..b73ead05 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -18,6 +18,15 @@ use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; /// Checks all type items in topological order. +/// +/// The order is determined by [`GroupedModule::type_scc`] in [`lowering`]. +/// The algorithm accounts for items that appear in [`RecursiveKinds`] by +/// filtering and populating these items with [`Type::Unknown`]. This +/// enables checking of other binding groups, which may be unaffected. +/// +/// [`GroupedModule::type_scc`]: lowering::GroupedModule::type_scc +/// [`RecursiveKinds`]: LoweringError::RecursiveKinds +/// [`Type::Unknown`]: crate::core::Type::Unknown pub fn check_type_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> where Q: ExternalQueries, From c85f3a5963562f71bc96ea37ea40107f7de0c74b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 22 Feb 2026 03:17:36 +0800 Subject: [PATCH 221/386] Improve type signature and variable checking --- compiler-core/checking2/src/context.rs | 5 + compiler-core/checking2/src/source.rs | 148 +++++++++--------- .../checking2/src/source/signature.rs | 41 +++++ 3 files changed, 124 insertions(+), 70 deletions(-) create mode 100644 compiler-core/checking2/src/source/signature.rs diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index d53f66f3..56b8c5ec 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -167,6 +167,11 @@ where self.queries.intern_type(Type::Function(argument, result)) } + /// Interns a right-associated function chain from arguments to result. + pub fn intern_function_chain(&self, arguments: &[TypeId], result: TypeId) -> TypeId { + arguments.iter().rfold(result, |result, &argument| self.intern_function(argument, result)) + } + /// Interns a [`Type::Kinded`] node. pub fn intern_kinded(&self, inner: TypeId, kind: TypeId) -> TypeId { self.queries.intern_type(Type::Kinded(inner, kind)) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index b73ead05..0e759bed 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -1,11 +1,13 @@ //! Implements syntax-driven checking rules for source files. +pub mod signature; pub mod synonym; pub mod terms; pub mod types; use building_types::QueryResult; use indexing::TypeItemId; +use itertools::Itertools; use lowering::{ DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, TypeVariableBinding, @@ -13,8 +15,8 @@ use lowering::{ use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, generalise, toolkit, unification, zonk}; -use crate::error::{ErrorCrumb, ErrorKind}; +use crate::core::{TypeId, generalise, unification, zonk}; +use crate::error::ErrorCrumb; use crate::state::CheckState; /// Checks all type items in topological order. @@ -232,19 +234,12 @@ fn check_data_equation( where Q: ExternalQueries, { - if let Some(signature_id) = signature { - let (matched_arguments, result) = - check_data_equation_variables(state, context, signature_id, item_id, variables)?; - let _ = build_data_equation_kind(state, context, variables, &matched_arguments, result)?; + if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + check_data_equation_check(state, context, (signature_id, signature_kind), variables)?; } else { - let inferred_kind = - build_data_equation_kind(state, context, variables, &[], context.prim.t)?; - - if let Some(known_kind) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, inferred_kind, known_kind)?; - } else { - state.checked.types.insert(item_id, inferred_kind); - } + check_data_equation_infer(state, context, item_id, variables)?; } check_data_constructor_arguments(state, context, item_id)?; @@ -252,83 +247,96 @@ where Ok(()) } -fn build_data_equation_kind( +fn check_data_equation_check( state: &mut CheckState, context: &CheckContext, - variables: &[TypeVariableBinding], - matched_arguments: &[TypeId], - result: TypeId, -) -> QueryResult + signature: (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult<()> where Q: ExternalQueries, { - let mut variable_kinds = Vec::with_capacity(variables.len()); - - for (index, binding) in variables.iter().enumerate() { - let expected_kind = matched_arguments.get(index).copied(); - - let kind = if let Some(expected_kind) = expected_kind { - if let Some(kind_id) = binding.kind { - let (kind, _) = types::infer_kind(state, context, kind_id)?; - let valid = unification::subtype(state, context, expected_kind, kind)?; - if valid { kind } else { context.unknown("invalid data equation variable kind") } - } else { - expected_kind - } - } else { - if let Some(kind_id) = binding.kind { - let (kind, _) = types::check_kind(state, context, kind_id, context.prim.t)?; - kind - } else { - state.fresh_unification(context.queries, context.prim.t) - } - }; + let signature = signature::inspect_signature(state, context, signature, &bindings)?; + let _ = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; + Ok(()) +} + +fn check_type_variable_bindings( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + signature: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut binding_kinds = Vec::with_capacity(bindings.len()); + + for (index, equation_binding) in bindings.iter().enumerate() { + let signature_kind = signature.get(index).copied(); + + let resolved_kind = + resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; let name = state.names.fresh(); - state.kind_scope.bind_forall(binding.id, name, kind); - variable_kinds.push(kind); + state.kind_scope.bind_forall(equation_binding.id, name, resolved_kind); + + binding_kinds.push(resolved_kind); } - let item_kind = variable_kinds - .iter() - .rfold(result, |result, &argument| context.intern_function(argument, result)); + Ok(binding_kinds) +} - Ok(item_kind) +fn resolve_type_variable_binding( + state: &mut CheckState, + context: &CheckContext, + signature: Option, + binding: &TypeVariableBinding, +) -> QueryResult +where + Q: ExternalQueries, +{ + match (signature, binding.kind) { + (Some(signature_kind), Some(binding_kind)) => { + let (binding_kind, _) = types::infer_kind(state, context, binding_kind)?; + let valid = unification::subtype(state, context, signature_kind, binding_kind)?; + if valid { Ok(binding_kind) } else { Ok(context.unknown("invalid variable kind")) } + } + (Some(signature_kind), None) => { + // Pure checking + Ok(signature_kind) + } + (None, Some(binding_kind)) => { + let (binding_kind, _) = + types::check_kind(state, context, binding_kind, context.prim.t)?; + Ok(binding_kind) + } + (None, None) => { + // Pure inference + Ok(state.fresh_unification(context.queries, context.prim.t)) + } + } } -fn check_data_equation_variables( +fn check_data_equation_infer( state: &mut CheckState, context: &CheckContext, - signature_id: lowering::TypeId, item_id: TypeItemId, - variables: &[TypeVariableBinding], -) -> QueryResult<(Vec, TypeId)> + bindings: &[TypeVariableBinding], +) -> QueryResult<()> where Q: ExternalQueries, { - let Some(stored_kind) = state.checked.lookup_type(item_id) else { - return Ok((vec![], context.unknown("missing checked kind for type item"))); - }; - - let (arguments, result) = toolkit::function_components(state, context, stored_kind)?; + let variable_kinds = check_type_variable_bindings(state, context, bindings, &[])?; + let inferred_kind = context.intern_function_chain(&variable_kinds, context.prim.t); - if variables.len() > arguments.len() { - state.insert_error(ErrorKind::TypeSignatureVariableMismatch { - id: signature_id, - expected: arguments.len() as u32, - actual: variables.len() as u32, - }); + if let Some(known_kind) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred_kind, known_kind)?; + } else { + state.checked.types.insert(item_id, inferred_kind); } - let matched_count = variables.len().min(arguments.len()); - let matched_arguments = arguments.iter().take(matched_count).copied().collect::>(); - - let final_kind = arguments - .iter() - .skip(matched_count) - .rfold(result, |result, &argument| context.intern_function(argument, result)); - - Ok((matched_arguments, final_kind)) + Ok(()) } fn check_data_constructor_arguments( diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs new file mode 100644 index 00000000..0232b116 --- /dev/null +++ b/compiler-core/checking2/src/source/signature.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use building_types::QueryResult; +use lowering::{TypeVariableBinding}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{TypeId, toolkit}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +pub struct InspectSignature { + pub arguments: Arc<[TypeId]>, + pub result: TypeId, +} + +pub fn inspect_signature( + state: &mut CheckState, + context: &CheckContext, + signature: (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult +where + Q: ExternalQueries, +{ + let (signature_id, signature_kind) = signature; + let (arguments, result) = toolkit::function_components(state, context, signature_kind)?; + + if bindings.len() > arguments.len() { + state.insert_error(ErrorKind::TypeSignatureVariableMismatch { + id: signature_id, + expected: arguments.len() as u32, + actual: bindings.len() as u32, + }); + } + + let count = bindings.len().min(arguments.len()); + let arguments = arguments.iter().take(count).copied().collect(); + + Ok(InspectSignature { arguments, result }) +} From 523cc10303389a2da372a8a4e4254cefbf1c28ce Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 22 Feb 2026 22:12:36 +0800 Subject: [PATCH 222/386] Rename to check_data_constructors --- compiler-core/checking2/src/source.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 0e759bed..7b5e827b 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -242,7 +242,7 @@ where check_data_equation_infer(state, context, item_id, variables)?; } - check_data_constructor_arguments(state, context, item_id)?; + check_data_constructors(state, context, item_id)?; Ok(()) } @@ -339,7 +339,7 @@ where Ok(()) } -fn check_data_constructor_arguments( +fn check_data_constructors( state: &mut CheckState, context: &CheckContext, item_id: TypeItemId, From b65f3bdae21fb15c0d5f20c5c9a8158d77936277 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 15:08:13 +0800 Subject: [PATCH 223/386] Make generalise solve unification variables to rigid variables --- .../checking2/src/core/generalise.rs | 8 ++-- .../checking2/src/core/substitute.rs | 41 ------------------- compiler-core/checking2/src/core/zonk.rs | 2 +- compiler-core/checking2/src/source.rs | 1 - 4 files changed, 4 insertions(+), 48 deletions(-) diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 96bfb7c2..e87906dd 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -24,8 +24,7 @@ use petgraph::prelude::DiGraphMap; use rustc_hash::FxHashSet; use smol_str::SmolStr; -use crate::core::substitute::{SubstituteUnification, UnificationToType}; -use crate::core::{ForallBinder, Type, TypeId, normalise}; +use crate::core::{ForallBinder, Type, TypeId, normalise, zonk}; use crate::ExternalQueries; use crate::context::CheckContext; @@ -152,7 +151,6 @@ where }; let mut quantified = id; - let mut substitutions = UnificationToType::default(); // All rigid type variables in a single generalisation share the same // depth, one level deeper than the ambient scope. Note that the depth @@ -187,8 +185,8 @@ where quantified = context.intern_forall(binder, quantified); let rigid = context.intern_rigid(name, depth, kind); - substitutions.insert(unification_id, rigid); + state.unifications.solve(unification_id, rigid); } - SubstituteUnification::on(state, context, &substitutions, quantified) + zonk::zonk(state, context, quantified) } diff --git a/compiler-core/checking2/src/core/substitute.rs b/compiler-core/checking2/src/core/substitute.rs index 684a0b99..2d4c494b 100644 --- a/compiler-core/checking2/src/core/substitute.rs +++ b/compiler-core/checking2/src/core/substitute.rs @@ -11,7 +11,6 @@ use crate::core::{Name, Type, TypeId}; use crate::state::CheckState; pub type NameToType = FxHashMap; -pub type UnificationToType = FxHashMap; /// Implements [`Name`]-based substitution for [`Type::Rigid`] variables. /// @@ -70,43 +69,3 @@ impl TypeFold for SubstituteName { } } } - -/// Implements substitution for [`Type::Unification`] variables. -pub struct SubstituteUnification<'a> { - substitutions: &'a UnificationToType, -} - -impl<'a> SubstituteUnification<'a> { - pub fn on( - state: &mut CheckState, - context: &CheckContext, - substitutions: &'a UnificationToType, - in_type: TypeId, - ) -> QueryResult - where - Q: ExternalQueries, - { - fold_type(state, context, in_type, &mut SubstituteUnification { substitutions }) - } -} - -impl TypeFold for SubstituteUnification<'_> { - fn transform( - &mut self, - _state: &mut CheckState, - _context: &CheckContext, - _id: TypeId, - t: &Type, - ) -> QueryResult - where - Q: ExternalQueries, - { - if let Type::Unification(unification_id) = t - && let Some(replacement) = self.substitutions.get(unification_id) - { - Ok(FoldAction::ReplaceThen(*replacement)) - } else { - Ok(FoldAction::Continue) - } - } -} diff --git a/compiler-core/checking2/src/core/zonk.rs b/compiler-core/checking2/src/core/zonk.rs index bef59170..bfc20f18 100644 --- a/compiler-core/checking2/src/core/zonk.rs +++ b/compiler-core/checking2/src/core/zonk.rs @@ -6,7 +6,7 @@ use crate::core::fold::{FoldAction, TypeFold, fold_type}; use crate::core::{Type, TypeId}; use crate::state::CheckState; -pub struct Zonk; +struct Zonk; impl TypeFold for Zonk { fn transform( diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 7b5e827b..6c07db5e 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -7,7 +7,6 @@ pub mod types; use building_types::QueryResult; use indexing::TypeItemId; -use itertools::Itertools; use lowering::{ DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, TypeVariableBinding, From b5cbcdff217df141a82a21eca51491991ea053ed Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 16:22:36 +0800 Subject: [PATCH 224/386] Accept IntoIter for intern_function_chain --- compiler-core/checking2/src/context.rs | 10 ++++++++-- compiler-core/checking2/src/source.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index 56b8c5ec..6f7fc115 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -168,8 +168,14 @@ where } /// Interns a right-associated function chain from arguments to result. - pub fn intern_function_chain(&self, arguments: &[TypeId], result: TypeId) -> TypeId { - arguments.iter().rfold(result, |result, &argument| self.intern_function(argument, result)) + pub fn intern_function_chain(&self, arguments: I, result: TypeId) -> TypeId + where + I: IntoIterator, + I::IntoIter: DoubleEndedIterator + { + arguments + .into_iter() + .rfold(result, |result, argument| self.intern_function(argument, result)) } /// Interns a [`Type::Kinded`] node. diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 6c07db5e..d5966cfb 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -327,7 +327,7 @@ where Q: ExternalQueries, { let variable_kinds = check_type_variable_bindings(state, context, bindings, &[])?; - let inferred_kind = context.intern_function_chain(&variable_kinds, context.prim.t); + let inferred_kind = context.intern_function_chain(variable_kinds, context.prim.t); if let Some(known_kind) = state.checked.lookup_type(item_id) { unification::subtype(state, context, inferred_kind, known_kind)?; From d0aed3debf9a714beab50eee40ea08dd72c83df9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 16:56:41 +0800 Subject: [PATCH 225/386] Collect ForallBinder in check_type_variable_bindings --- compiler-core/checking2/src/source.rs | 31 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index d5966cfb..002e48ad 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -11,10 +11,11 @@ use lowering::{ DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, TypeVariableBinding, }; +use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, generalise, unification, zonk}; +use crate::core::{ForallBinder, TypeId, generalise, unification, zonk}; use crate::error::ErrorCrumb; use crate::state::CheckState; @@ -265,25 +266,28 @@ fn check_type_variable_bindings( context: &CheckContext, bindings: &[TypeVariableBinding], signature: &[TypeId], -) -> QueryResult> +) -> QueryResult> where Q: ExternalQueries, { - let mut binding_kinds = Vec::with_capacity(bindings.len()); + let mut binders = vec![]; for (index, equation_binding) in bindings.iter().enumerate() { let signature_kind = signature.get(index).copied(); - let resolved_kind = - resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; + let kind = resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; let name = state.names.fresh(); - state.kind_scope.bind_forall(equation_binding.id, name, resolved_kind); + state.kind_scope.bind_forall(equation_binding.id, name, kind); - binding_kinds.push(resolved_kind); + const MISSING: SmolStr = SmolStr::new_static(""); + let text = equation_binding.name.clone().unwrap_or(MISSING); + let visible = equation_binding.visible; + + binders.push(ForallBinder { visible, name, text, kind }); } - Ok(binding_kinds) + Ok(binders) } fn resolve_type_variable_binding( @@ -326,13 +330,14 @@ fn check_data_equation_infer( where Q: ExternalQueries, { - let variable_kinds = check_type_variable_bindings(state, context, bindings, &[])?; - let inferred_kind = context.intern_function_chain(variable_kinds, context.prim.t); + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.into_iter().map(|binder| binder.kind); + let inferred = context.intern_function_chain(kinds, context.prim.t); - if let Some(known_kind) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, inferred_kind, known_kind)?; + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; } else { - state.checked.types.insert(item_id, inferred_kind); + state.checked.types.insert(item_id, inferred); } Ok(()) From 635dc106d93fdf71dc804821ea673b7e0a21d0ac Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 16:58:23 +0800 Subject: [PATCH 226/386] Return ForallBinder in data check and infer modes --- compiler-core/checking2/src/source.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 002e48ad..8696d515 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -234,13 +234,13 @@ fn check_data_equation( where Q: ExternalQueries, { - if let Some(signature_id) = signature + let _ = if let Some(signature_id) = signature && let Some(signature_kind) = state.checked.lookup_type(item_id) { - check_data_equation_check(state, context, (signature_id, signature_kind), variables)?; + check_data_equation_check(state, context, (signature_id, signature_kind), variables)? } else { - check_data_equation_infer(state, context, item_id, variables)?; - } + check_data_equation_infer(state, context, item_id, variables)? + }; check_data_constructors(state, context, item_id)?; @@ -252,13 +252,12 @@ fn check_data_equation_check( context: &CheckContext, signature: (lowering::TypeId, TypeId), bindings: &[TypeVariableBinding], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { let signature = signature::inspect_signature(state, context, signature, &bindings)?; - let _ = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; - Ok(()) + check_type_variable_bindings(state, context, bindings, &signature.arguments) } fn check_type_variable_bindings( @@ -326,12 +325,12 @@ fn check_data_equation_infer( context: &CheckContext, item_id: TypeItemId, bindings: &[TypeVariableBinding], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { let bindings = check_type_variable_bindings(state, context, bindings, &[])?; - let kinds = bindings.into_iter().map(|binder| binder.kind); + let kinds = bindings.iter().map(|binder| binder.kind); let inferred = context.intern_function_chain(kinds, context.prim.t); if let Some(expected) = state.checked.lookup_type(item_id) { @@ -340,7 +339,7 @@ where state.checked.types.insert(item_id, inferred); } - Ok(()) + Ok(bindings) } fn check_data_constructors( From 27a39bb1b850891a0032d7b099479d2be697bc3a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 14:47:59 +0800 Subject: [PATCH 227/386] Add toolkit functions for inspection --- compiler-core/checking2/src/core/toolkit.rs | 47 +++++++++++++++---- .../checking2/src/source/signature.rs | 18 ++++--- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index f443cf96..c29c4897 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -3,16 +3,50 @@ use building_types::QueryResult; use crate::context::CheckContext; -use crate::core::{Type, TypeId, normalise}; +use crate::core::{ForallBinder, Type, TypeId, normalise}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; -/// Splits a function-like type into argument kinds and a result kind. -pub fn function_components( +pub struct InspectQuantified { + pub binders: Vec, + pub quantified: TypeId, +} + +pub struct InspectFunction { + pub arguments: Vec, + pub result: TypeId, +} + +pub fn inspect_quantified( state: &mut CheckState, context: &CheckContext, id: TypeId, -) -> QueryResult<(Vec, TypeId)> +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut binders = vec![]; + let mut current = id; + + safe_loop! { + current = normalise::normalise(state, context, current)?; + + let Type::Forall(binder_id, inner) = context.lookup_type(current) else { + break; + }; + + binders.push(context.lookup_forall_binder(binder_id)); + current = inner; + } + + Ok(InspectQuantified { binders, quantified: current }) +} + +pub fn inspect_function( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult where Q: ExternalQueries, { @@ -23,9 +57,6 @@ where current = normalise::normalise(state, context, current)?; match context.lookup_type(current) { - Type::Forall(_, inner) => { - current = inner; - } Type::Function(argument, result) => { arguments.push(argument); current = result; @@ -47,5 +78,5 @@ where } } - Ok((arguments, current)) + Ok(InspectFunction { arguments, result: current }) } diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs index 0232b116..75755c70 100644 --- a/compiler-core/checking2/src/source/signature.rs +++ b/compiler-core/checking2/src/source/signature.rs @@ -1,16 +1,15 @@ -use std::sync::Arc; - use building_types::QueryResult; -use lowering::{TypeVariableBinding}; +use lowering::TypeVariableBinding; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, toolkit}; +use crate::core::{ForallBinder, TypeId, toolkit}; use crate::error::ErrorKind; use crate::state::CheckState; pub struct InspectSignature { - pub arguments: Arc<[TypeId]>, + pub binders: Vec, + pub arguments: Vec, pub result: TypeId, } @@ -24,7 +23,12 @@ where Q: ExternalQueries, { let (signature_id, signature_kind) = signature; - let (arguments, result) = toolkit::function_components(state, context, signature_kind)?; + + let toolkit::InspectQuantified { binders, quantified } = + toolkit::inspect_quantified(state, context, signature_kind)?; + + let toolkit::InspectFunction { arguments, result } = + toolkit::inspect_function(state, context, quantified)?; if bindings.len() > arguments.len() { state.insert_error(ErrorKind::TypeSignatureVariableMismatch { @@ -37,5 +41,5 @@ where let count = bindings.len().min(arguments.len()); let arguments = arguments.iter().take(count).copied().collect(); - Ok(InspectSignature { arguments, result }) + Ok(InspectSignature { binders, arguments, result }) } From 108ff9c801b87d6a89be42771940973ae5956cf0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 16:19:14 +0800 Subject: [PATCH 228/386] Add terms field for CheckState --- compiler-core/checking2/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 5e86f875..8b547e02 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use building_types::{QueryProxy, QueryResult}; use files::FileId; -use indexing::TypeItemId; +use indexing::{TermItemId, TypeItemId}; use resolving::ResolvedModule; use rustc_hash::FxHashMap; use smol_str::SmolStr; @@ -52,6 +52,7 @@ pub trait ExternalQueries: #[derive(Debug, Default, PartialEq, Eq)] pub struct CheckedModule { pub types: FxHashMap, + pub terms: FxHashMap, pub synonyms: FxHashMap, pub roles: FxHashMap>, pub errors: Vec, @@ -62,6 +63,10 @@ impl CheckedModule { self.types.get(&id).copied() } + pub fn lookup_term(&self, id: TermItemId) -> Option { + self.terms.get(&id).copied() + } + pub fn lookup_synonym(&self, id: TypeItemId) -> Option { self.synonyms.get(&id).cloned() } From 6f848815495036c4d8a84a0a7e9d55ec1f9cd86b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 14:48:02 +0800 Subject: [PATCH 229/386] Implement generalisation for constructors --- compiler-core/checking2/src/source.rs | 125 ++++++++++++++++-- .../checking2/src/source/signature.rs | 3 + compiler-core/checking2/src/state.rs | 2 +- 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 8696d515..cde061f5 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -6,7 +6,7 @@ pub mod terms; pub mod types; use building_types::QueryResult; -use indexing::TypeItemId; +use indexing::{TermItemId, TypeItemId}; use lowering::{ DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, TypeVariableBinding, @@ -15,10 +15,20 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, TypeId, generalise, unification, zonk}; +use crate::core::{ForallBinder, Type, TypeId, generalise, toolkit, unification, zonk}; use crate::error::ErrorCrumb; use crate::state::CheckState; +struct PendingDataType { + parameters: Vec, + constructors: Vec<(TermItemId, Vec)>, +} + +#[derive(Default)] +struct TypeSccState { + data: Vec<(TypeItemId, PendingDataType)>, +} + /// Checks all type items in topological order. /// /// The order is determined by [`GroupedModule::type_scc`] in [`lowering`]. @@ -45,11 +55,14 @@ where prepare_binding_group(state, context, &items); } + let mut scc_state = TypeSccState { data: vec![] }; + for &item in &items { - check_type_equation(state, context, item)?; + check_type_equation(state, context, &mut scc_state, item)?; } finalise_binding_group(state, context, &items)?; + finalise_data_constructors(state, context, scc_state)?; } Ok(()) } @@ -197,6 +210,7 @@ where fn check_type_equation( state: &mut CheckState, context: &CheckContext, + scc: &mut TypeSccState, item_id: TypeItemId, ) -> QueryResult<()> where @@ -209,11 +223,11 @@ where match item { TypeItemIr::DataGroup { signature, data, .. } => { let Some(DataIr { variables }) = data else { return Ok(()) }; - check_data_equation(state, context, item_id, *signature, &variables)?; + check_data_equation(state, context, scc, item_id, *signature, &variables)?; } TypeItemIr::NewtypeGroup { signature, newtype, .. } => { let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; - check_data_equation(state, context, item_id, *signature, &variables)?; + check_data_equation(state, context, scc, item_id, *signature, &variables)?; } TypeItemIr::SynonymGroup { .. } => todo!(), TypeItemIr::ClassGroup { .. } => todo!(), @@ -227,6 +241,7 @@ where fn check_data_equation( state: &mut CheckState, context: &CheckContext, + scc: &mut TypeSccState, item_id: TypeItemId, signature: Option, variables: &[TypeVariableBinding], @@ -234,7 +249,7 @@ fn check_data_equation( where Q: ExternalQueries, { - let _ = if let Some(signature_id) = signature + let parameters = if let Some(signature_id) = signature && let Some(signature_kind) = state.checked.lookup_type(item_id) { check_data_equation_check(state, context, (signature_id, signature_kind), variables)? @@ -242,7 +257,8 @@ where check_data_equation_infer(state, context, item_id, variables)? }; - check_data_constructors(state, context, item_id)?; + let constructors = check_data_constructors(state, context, item_id)?; + scc.data.push((item_id, PendingDataType { parameters, constructors })); Ok(()) } @@ -346,10 +362,12 @@ fn check_data_constructors( state: &mut CheckState, context: &CheckContext, item_id: TypeItemId, -) -> QueryResult<()> +) -> QueryResult)>> where Q: ExternalQueries, { + let mut constructors = vec![]; + for constructor_id in context.indexed.pairs.data_constructors(item_id) { let Some(TermItemIr::Constructor { arguments }) = context.lowered.info.get_term_item(constructor_id) @@ -357,12 +375,101 @@ where continue; }; + let mut checked_arguments = vec![]; for &argument in arguments.iter() { state.with_error_crumb(ErrorCrumb::ConstructorArgument(argument), |state| { - let (_, _) = types::check_kind(state, context, argument, context.prim.t)?; + let (checked_argument, _) = + types::check_kind(state, context, argument, context.prim.t)?; + checked_arguments.push(checked_argument); Ok(()) })?; } + constructors.push((constructor_id, checked_arguments)); + } + + Ok(constructors) +} + +fn finalise_data_constructors( + state: &mut CheckState, + context: &CheckContext, + scc: TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, PendingDataType { parameters, constructors }) in scc.data { + // constructor_kind should have already been generalised by the + // finalise_binding_group function. the kind signature is used + // as the source of truth for constructing the kind applications + let Some(constructor_kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let toolkit::InspectQuantified { binders: kind_binders, quantified } = + toolkit::inspect_quantified(state, context, constructor_kind)?; + + let toolkit::InspectFunction { arguments: parameter_kinds, .. } = + toolkit::inspect_function(state, context, quantified)?; + + // parameter_kinds is the post-generalisation kind for each parameter; + // we want to replace pre-generalisation kinds carried by parameters + // before constructing the signature for the constructor. + let get_parameter_kind = |index: usize| { + if let Some(kind) = parameter_kinds.get(index) { + *kind + } else { + context.unknown("invalid kind") + } + }; + + let type_reference = context.queries.intern_type(Type::Constructor(context.id, item_id)); + + // For the following code loop, let's trace through the declaration: + // + // newtype Tagged :: forall k. k -> Type -> Type + // + for (constructor_id, checked_arguments) in constructors { + let mut result = type_reference; + + // Tagged @k + for binder in &kind_binders { + let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); + result = context.intern_kind_application(result, rigid); + } + + // Tagged @k t a + for (index, parameter) in parameters.iter().enumerate() { + let kind = get_parameter_kind(index); + let rigid = context.intern_rigid(parameter.name, state.depth, kind); + result = context.intern_application(result, rigid); + } + + // a -> Tagged @k t a + for argument in checked_arguments.into_iter().rev() { + let argument = zonk::zonk(state, context, argument)?; + result = context.intern_function(argument, result); + } + + // forall (a :: Type). a -> Tagged @k t a + for (index, parameter) in parameters.iter().enumerate().rev() { + let kind = get_parameter_kind(index); + + let parameter = ForallBinder::clone(parameter); + let binder = ForallBinder { kind, ..parameter }; + + let binder_id = context.intern_forall_binder(binder); + result = context.intern_forall(binder_id, result); + } + + // forall (k :: Type) (t :: k) (a :: Type). a -> Tagged @k t a + for binder in kind_binders.iter().rev() { + let binder_id = context.intern_forall_binder(binder.clone()); + result = context.intern_forall(binder_id, result); + } + + state.checked.terms.insert(constructor_id, result); + } } Ok(()) diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs index 75755c70..ec552beb 100644 --- a/compiler-core/checking2/src/source/signature.rs +++ b/compiler-core/checking2/src/source/signature.rs @@ -13,6 +13,9 @@ pub struct InspectSignature { pub result: TypeId, } +// TODO: Inline into check_data_equation_check; inspect_quantified and +// inspect_function are still needed for error reporting (mismatch), but +// InspectSignature can be collapsed to just Vec for arguments. pub fn inspect_signature( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 7845bbdd..de205a7d 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -7,8 +7,8 @@ use files::FileId; use rustc_hash::FxHashMap; use crate::context::CheckContext; -use crate::core::zonk; use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty}; +use crate::core::zonk; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; From e94d195fd976b9191d48fbbe708d62e65111d186 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 20:29:48 +0800 Subject: [PATCH 230/386] Format files and apply clippy --- compiler-core/checking2/src/context.rs | 2 +- compiler-core/checking2/src/source.rs | 6 +++--- compiler-core/checking2/src/state.rs | 3 +-- compiler-core/lowering/src/lib.rs | 3 ++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index 6f7fc115..a2b7cc9b 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -171,7 +171,7 @@ where pub fn intern_function_chain(&self, arguments: I, result: TypeId) -> TypeId where I: IntoIterator, - I::IntoIter: DoubleEndedIterator + I::IntoIter: DoubleEndedIterator, { arguments .into_iter() diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index cde061f5..5f4ee42e 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -223,11 +223,11 @@ where match item { TypeItemIr::DataGroup { signature, data, .. } => { let Some(DataIr { variables }) = data else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, &variables)?; + check_data_equation(state, context, scc, item_id, *signature, variables)?; } TypeItemIr::NewtypeGroup { signature, newtype, .. } => { let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, &variables)?; + check_data_equation(state, context, scc, item_id, *signature, variables)?; } TypeItemIr::SynonymGroup { .. } => todo!(), TypeItemIr::ClassGroup { .. } => todo!(), @@ -272,7 +272,7 @@ fn check_data_equation_check( where Q: ExternalQueries, { - let signature = signature::inspect_signature(state, context, signature, &bindings)?; + let signature = signature::inspect_signature(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index de205a7d..cf8c9d5f 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -7,8 +7,7 @@ use files::FileId; use rustc_hash::FxHashMap; use crate::context::CheckContext; -use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty}; -use crate::core::zonk; +use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty, zonk}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; diff --git a/compiler-core/lowering/src/lib.rs b/compiler-core/lowering/src/lib.rs index 841b722e..5d5c3fc0 100644 --- a/compiler-core/lowering/src/lib.rs +++ b/compiler-core/lowering/src/lib.rs @@ -8,8 +8,9 @@ pub mod intermediate; pub mod scope; pub mod source; +use std::hash::Hash; +use std::slice; use std::sync::Arc; -use std::{hash::Hash, slice}; pub use error::*; pub use intermediate::*; From 7df9a5dbbaf82dc9dec3243962aa7502758bbe87 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 21:06:04 +0800 Subject: [PATCH 231/386] Add CoreInterners struct for checking2 --- compiler-core/checking2/src/interners.rs | 90 ++++++++++++++++++++++++ compiler-core/checking2/src/lib.rs | 3 + 2 files changed, 93 insertions(+) create mode 100644 compiler-core/checking2/src/interners.rs diff --git a/compiler-core/checking2/src/interners.rs b/compiler-core/checking2/src/interners.rs new file mode 100644 index 00000000..0412d638 --- /dev/null +++ b/compiler-core/checking2/src/interners.rs @@ -0,0 +1,90 @@ +use std::ops; + +use smol_str::SmolStr; + +use crate::core::{ + ForallBinder, ForallBinderId, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, +}; + +#[derive(Default)] +pub struct CoreInterners { + types: interner::Interner, + forall_binders: interner::Interner, + row_types: interner::Interner, + synonyms: interner::Interner, + smol_strs: interner::Interner, +} + +impl CoreInterners { + pub fn intern_type(&mut self, t: Type) -> TypeId { + self.types.intern(t) + } + + pub fn lookup_type(&self, id: TypeId) -> Type { + self.types[id].clone() + } + + pub fn intern_forall_binder(&mut self, b: ForallBinder) -> ForallBinderId { + self.forall_binders.intern(b) + } + + pub fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder { + self.forall_binders[id].clone() + } + + pub fn intern_row_type(&mut self, r: RowType) -> RowTypeId { + self.row_types.intern(r) + } + + pub fn lookup_row_type(&self, id: RowTypeId) -> RowType { + self.row_types[id].clone() + } + + pub fn intern_synonym(&mut self, s: Synonym) -> SynonymId { + self.synonyms.intern(s) + } + + pub fn lookup_synonym(&self, id: SynonymId) -> Synonym { + self.synonyms[id].clone() + } + + pub fn intern_smol_str(&mut self, s: SmolStr) -> crate::core::SmolStrId { + self.smol_strs.intern(s) + } + + pub fn lookup_smol_str(&self, id: crate::core::SmolStrId) -> SmolStr { + self.smol_strs[id].clone() + } +} + +impl ops::Index for CoreInterners { + type Output = Type; + + fn index(&self, id: TypeId) -> &Type { + &self.types[id] + } +} + +impl ops::Index for CoreInterners { + type Output = ForallBinder; + + fn index(&self, id: ForallBinderId) -> &ForallBinder { + &self.forall_binders[id] + } +} + +impl ops::Index for CoreInterners { + type Output = RowType; + + fn index(&self, id: RowTypeId) -> &RowType { + &self.row_types[id] + } +} + +impl ops::Index for CoreInterners { + type Output = Synonym; + + fn index(&self, id: SynonymId) -> &Synonym { + &self.synonyms[id] + } +} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 8b547e02..6fe33ffa 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -2,10 +2,13 @@ pub mod context; pub mod core; pub mod error; pub mod implication; +pub mod interners; pub mod safety; pub mod source; pub mod state; +pub use interners::CoreInterners; + use std::sync::Arc; use building_types::{QueryProxy, QueryResult}; From 76d46504c45e6755c6e0cb006e333e35fb075850 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 21:00:14 +0800 Subject: [PATCH 232/386] Add initial support for checking2 in QueryEngine --- Cargo.lock | 2 + compiler-core/building-types/src/lib.rs | 1 + compiler-core/building/Cargo.toml | 2 + compiler-core/building/src/engine.rs | 78 +++++++++++++++++++ compiler-core/checking2/src/lib.rs | 3 +- compiler-core/checking2/src/source/synonym.rs | 2 +- compiler-core/checking2/src/source/types.rs | 2 +- 7 files changed, 87 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1244912..41d5be4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ version = "0.1.0" dependencies = [ "building-types", "checking", + "checking2", "files", "indexing", "interner", @@ -237,6 +238,7 @@ dependencies = [ "prim-constants", "resolving", "rustc-hash 2.1.1", + "smol_str", "stabilizing", "sugar", "tempfile", diff --git a/compiler-core/building-types/src/lib.rs b/compiler-core/building-types/src/lib.rs index 4332f1f4..ceaafe70 100644 --- a/compiler-core/building-types/src/lib.rs +++ b/compiler-core/building-types/src/lib.rs @@ -19,6 +19,7 @@ pub enum QueryKey { Bracketed(FileId), Sectioned(FileId), Checked(FileId), + Checked2(FileId), } #[derive(Error, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/compiler-core/building/Cargo.toml b/compiler-core/building/Cargo.toml index 1b118af3..586d9fc9 100644 --- a/compiler-core/building/Cargo.toml +++ b/compiler-core/building/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] building-types = { version = "0.1.0", path = "../building-types" } checking = { version = "0.1.0", path = "../checking" } +checking2 = { version = "0.1.0", path = "../checking2" } files = { version = "0.1.0", path = "../files" } indexing = { version = "0.1.0", path = "../indexing" } interner = { version = "0.1.0", path = "../interner" } @@ -19,6 +20,7 @@ resolving = { version = "0.1.0", path = "../resolving" } rustc-hash = "2.1.1" stabilizing = { version = "0.1.0", path = "../stabilizing" } sugar = { version = "0.1.0", path = "../sugar" } +smol_str = "0.3.2" tempfile = "3.24.0" thread_local = "1.1.9" url = "2.5.7" diff --git a/compiler-core/building/src/engine.rs b/compiler-core/building/src/engine.rs index a26d0436..c3ea1a50 100644 --- a/compiler-core/building/src/engine.rs +++ b/compiler-core/building/src/engine.rs @@ -34,6 +34,7 @@ use building_types::{ ModuleNameId, ModuleNameInterner, QueryError, QueryKey, QueryProxy, QueryResult, }; use checking::{CheckedModule, TypeInterner}; +use checking2::CheckedModule as CheckedModule2; use files::FileId; use graph::SnapshotGraph; use indexing::IndexedModule; @@ -99,12 +100,14 @@ struct DerivedStorage { bracketed: FxHashMap>>, sectioned: FxHashMap>>, checked: FxHashMap>>, + checked2: FxHashMap>>, } #[derive(Default)] struct InternedStorage { module: ModuleNameInterner, types: TypeInterner, + checking2: checking2::CoreInterners, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -428,6 +431,7 @@ impl QueryEngine { QueryKey::Bracketed(k) => derived_changed!(bracketed, k), QueryKey::Sectioned(k) => derived_changed!(sectioned, k), QueryKey::Checked(k) => derived_changed!(checked, k), + QueryKey::Checked2(k) => derived_changed!(checked2, k), } } @@ -757,6 +761,18 @@ impl QueryEngine { }, ) } + + pub fn checked2(&self, id: FileId) -> QueryResult> { + self.query( + QueryKey::Checked2(id), + |storage| storage.derived.checked2.get(&id), + |storage| storage.derived.checked2.entry(id), + |this| { + let checked = checking2::check_module(this, id)?; + Ok(Arc::new(checked)) + }, + ) + } } impl QueryEngine { @@ -841,6 +857,68 @@ impl checking::ExternalQueries for QueryEngine { } } +impl checking2::ExternalQueries for QueryEngine { + fn checked2(&self, id: FileId) -> QueryResult> { + QueryEngine::checked2(self, id) + } + + fn intern_type(&self, t: checking2::core::Type) -> checking2::core::TypeId { + let mut storage = self.storage.write(); + storage.interned.checking2.intern_type(t) + } + + fn lookup_type(&self, id: checking2::core::TypeId) -> checking2::core::Type { + let storage = self.storage.read(); + storage.interned.checking2.lookup_type(id) + } + + fn intern_forall_binder( + &self, + binder: checking2::core::ForallBinder, + ) -> checking2::core::ForallBinderId { + let mut storage = self.storage.write(); + storage.interned.checking2.intern_forall_binder(binder) + } + + fn lookup_forall_binder( + &self, + id: checking2::core::ForallBinderId, + ) -> checking2::core::ForallBinder { + let storage = self.storage.read(); + storage.interned.checking2.lookup_forall_binder(id) + } + + fn intern_row_type(&self, row: checking2::core::RowType) -> checking2::core::RowTypeId { + let mut storage = self.storage.write(); + storage.interned.checking2.intern_row_type(row) + } + + fn lookup_row_type(&self, id: checking2::core::RowTypeId) -> checking2::core::RowType { + let storage = self.storage.read(); + storage.interned.checking2.lookup_row_type(id) + } + + fn intern_synonym(&self, synonym: checking2::core::Synonym) -> checking2::core::SynonymId { + let mut storage = self.storage.write(); + storage.interned.checking2.intern_synonym(synonym) + } + + fn lookup_synonym(&self, id: checking2::core::SynonymId) -> checking2::core::Synonym { + let storage = self.storage.read(); + storage.interned.checking2.lookup_synonym(id) + } + + fn intern_smol_str(&self, s: smol_str::SmolStr) -> checking2::core::SmolStrId { + let mut storage = self.storage.write(); + storage.interned.checking2.intern_smol_str(s) + } + + fn lookup_smol_str(&self, id: checking2::core::SmolStrId) -> smol_str::SmolStr { + let storage = self.storage.read(); + storage.interned.checking2.lookup_smol_str(id) + } +} + impl resolving::ExternalQueries for QueryEngine {} impl sugar::ExternalQueries for QueryEngine {} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 6fe33ffa..d8b9422e 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -33,9 +33,10 @@ pub trait ExternalQueries: Resolved = Arc, Bracketed = Arc, Sectioned = Arc, - Checked = Arc, > { + fn checked2(&self, id: FileId) -> QueryResult>; + fn intern_type(&self, t: Type) -> TypeId; fn lookup_type(&self, id: TypeId) -> Type; diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 9d80f1a5..8cef5c35 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -27,7 +27,7 @@ where let (synonym, kind) = if file_id == context.id { (state.checked.lookup_synonym(type_id), state.checked.lookup_type(type_id)) } else { - let checked = context.queries.checked(file_id)?; + let checked = context.queries.checked2(file_id)?; (checked.lookup_synonym(type_id), checked.lookup_type(type_id)) }; diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index ee57cf41..4e8b005d 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -361,7 +361,7 @@ where let result = if file_id == context.id { state.checked.types.get(&type_id).copied() } else { - let checked = context.queries.checked(file_id)?; + let checked = context.queries.checked2(file_id)?; checked.types.get(&type_id).copied() }; Ok(result.unwrap_or_else(|| context.unknown("kind"))) From 1e00b738caac131da2da2be9b3f5ab49b4b7dc4a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 21:00:14 +0800 Subject: [PATCH 233/386] Add support for checking2 in integration tests --- Cargo.lock | 1 + compiler-scripts/src/lib.rs | 1 + compiler-scripts/src/main.rs | 2 +- compiler-scripts/src/test_runner/category.rs | 7 ++- justfile | 2 +- tests-integration/Cargo.toml | 1 + tests-integration/build.rs | 49 +++++++++++++++ tests-integration/fixtures/checking2/.gitkeep | 1 + tests-integration/src/generated/basic.rs | 63 +++++++++++++++++++ tests-integration/src/lib.rs | 2 +- tests-integration/tests/checking2.rs | 2 + .../tests/checking2/generated.rs | 31 +++++++++ 12 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 tests-integration/fixtures/checking2/.gitkeep create mode 100644 tests-integration/tests/checking2.rs create mode 100644 tests-integration/tests/checking2/generated.rs diff --git a/Cargo.lock b/Cargo.lock index 41d5be4f..4d5e195f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2638,6 +2638,7 @@ dependencies = [ "analyzer", "async-lsp", "checking", + "checking2", "diagnostics", "files", "glob", diff --git a/compiler-scripts/src/lib.rs b/compiler-scripts/src/lib.rs index 0ab89a1a..771fdd5b 100644 --- a/compiler-scripts/src/lib.rs +++ b/compiler-scripts/src/lib.rs @@ -56,6 +56,7 @@ pub mod fixtures { ("LOWERING_FIXTURES_HASH".into(), hash_fixtures(&base.join("lowering"))), ("RESOLVING_FIXTURES_HASH".into(), hash_fixtures(&base.join("resolving"))), ("CHECKING_FIXTURES_HASH".into(), hash_fixtures(&base.join("checking"))), + ("CHECKING2_FIXTURES_HASH".into(), hash_fixtures(&base.join("checking2"))), ]) } } diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs index 89d36188..d887731d 100644 --- a/compiler-scripts/src/main.rs +++ b/compiler-scripts/src/main.rs @@ -9,7 +9,7 @@ use compiler_scripts::test_runner::{ #[derive(Parser)] #[command(about = "Compiler development scripts")] struct Cli { - /// Test category: checking (c), lowering (l), resolving (r), lsp + /// Test category: checking (c), checking2 (c2), lowering (l), resolving (r), lsp category: TestCategory, #[command(flatten)] diff --git a/compiler-scripts/src/test_runner/category.rs b/compiler-scripts/src/test_runner/category.rs index 932770e2..c3d806c8 100644 --- a/compiler-scripts/src/test_runner/category.rs +++ b/compiler-scripts/src/test_runner/category.rs @@ -6,6 +6,7 @@ use anyhow::bail; #[derive(Copy, Clone, Debug)] pub enum TestCategory { Checking, + Checking2, Lowering, Resolving, Lsp, @@ -15,6 +16,7 @@ impl TestCategory { pub fn as_str(&self) -> &'static str { match self { TestCategory::Checking => "checking", + TestCategory::Checking2 => "checking2", TestCategory::Lowering => "lowering", TestCategory::Resolving => "resolving", TestCategory::Lsp => "lsp", @@ -38,7 +40,7 @@ impl TestCategory { pub fn trace_for_snapshot(&self, snap_path: &Path, trace_paths: &[PathBuf]) -> Option { match self { - TestCategory::Checking => { + TestCategory::Checking | TestCategory::Checking2 => { crate::test_runner::traces::match_checking_trace(snap_path, trace_paths) } _ => None, @@ -52,11 +54,12 @@ impl FromStr for TestCategory { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "checking" | "c" => Ok(TestCategory::Checking), + "checking2" | "c2" => Ok(TestCategory::Checking2), "lowering" | "l" => Ok(TestCategory::Lowering), "resolving" | "r" => Ok(TestCategory::Resolving), "lsp" => Ok(TestCategory::Lsp), _ => bail!( - "unknown test category '{}', expected: checking (c), lowering (l), resolving (r), lsp", + "unknown test category '{}', expected: checking (c), checking2 (c2), lowering (l), resolving (r), lsp", s ), } diff --git a/justfile b/justfile index ab4bfd95..cc25d299 100644 --- a/justfile +++ b/justfile @@ -24,7 +24,7 @@ coverage-html: @integration *args="": cargo nextest run -p tests-integration "$@" --status-level=fail --final-status-level=fail --failure-output=final -[doc("Run integration tests with snapshot diffing: checking|lowering|resolving|lsp")] +[doc("Run integration tests with snapshot diffing: checking|checking2|lowering|resolving|lsp")] @t *args="": cargo run -q -p compiler-scripts --release -- "$@" diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml index 48952bab..804a8880 100644 --- a/tests-integration/Cargo.toml +++ b/tests-integration/Cargo.toml @@ -8,6 +8,7 @@ build = "build.rs" analyzer = { version = "0.1.0", path = "../compiler-lsp/analyzer" } async-lsp = "0.2.2" checking = { version = "0.1.0", path = "../compiler-core/checking" } +checking2 = { version = "0.1.0", path = "../compiler-core/checking2" } diagnostics = { version = "0.1.0", path = "../compiler-core/diagnostics" } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] } diff --git a/tests-integration/build.rs b/tests-integration/build.rs index f76345e3..375ee46b 100644 --- a/tests-integration/build.rs +++ b/tests-integration/build.rs @@ -18,10 +18,12 @@ fn main() { println!("cargo::rerun-if-env-changed=LOWERING_FIXTURES_HASH"); println!("cargo::rerun-if-env-changed=RESOLVING_FIXTURES_HASH"); println!("cargo::rerun-if-env-changed=CHECKING_FIXTURES_HASH"); + println!("cargo::rerun-if-env-changed=CHECKING2_FIXTURES_HASH"); generate_lsp(); generate_lowering(); generate_resolving(); generate_checking(); + generate_checking2(); } fn generate_lsp() { @@ -169,3 +171,50 @@ fn run_test(folder: &str, file: &str) {{ .unwrap(); } } + +fn generate_checking2() { + let mut buffer = fs::File::create("./tests/checking2/generated.rs").unwrap(); + writeln!(buffer, r#"// Do not edit! See build.rs + +#[rustfmt::skip] +fn run_test(folder: &str, file: &str) {{ + let path = std::path::Path::new("fixtures/checking2").join(folder); + let (engine, _) = tests_integration::load_compiler(&path); + let Some(id) = engine.module_file(file) else {{ return }}; + + let level = match std::env::var("TRACE_LEVEL").as_deref() {{ + Ok("debug") => tracing::Level::DEBUG, + _ => tracing::Level::WARN, + }}; + + let target_dir = env!("CARGO_TARGET_TMPDIR"); + let test_name = format!("{{}}_{{}}", folder, file); + let (report, trace_path) = tests_integration::trace::with_file_trace( + level, + target_dir, + &test_name, + || tests_integration::generated::basic::report_checked2(&engine, id) + ); + + println!("trace: {{}}", trace_path.display()); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/checking2").join(folder)); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| insta::assert_snapshot!(file, report)); +}}"#).unwrap(); + + for folder in read_dir(Path::new("./fixtures/checking2")) { + let Some(stem) = folder.file_stem() else { continue }; + let folder_name = stem.to_os_string().into_string().unwrap().to_snake_case(); + if folder_name == "prelude" { + continue; + } + writeln!( + buffer, + r#" +#[rustfmt::skip] #[test] fn test_{folder_name}_main() {{ run_test("{folder_name}", "Main"); }}"# + ) + .unwrap(); + } +} diff --git a/tests-integration/fixtures/checking2/.gitkeep b/tests-integration/fixtures/checking2/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests-integration/fixtures/checking2/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 21abe995..bb278bc4 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,6 +2,8 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; +use checking2::ExternalQueries; +use checking2::core::pretty as pretty2; use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; use indexing::{ImportKind, TermItem, TypeItem, TypeItemId, TypeItemKind}; @@ -452,6 +454,67 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { snapshot } +pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { + let indexed = engine.indexed(id).unwrap(); + let checked = engine.checked2(id).unwrap(); + + let mut snapshot = String::default(); + + writeln!(snapshot, "Terms").unwrap(); + for (id, TermItem { name, .. }) in indexed.items.iter_terms() { + let Some(name) = name else { continue }; + let Some(kind) = checked.lookup_term(id) else { continue }; + let signature = pretty2::print_signature(engine, name, kind); + writeln!(snapshot, "{signature}").unwrap(); + } + + writeln!(snapshot, "\nTypes").unwrap(); + for (id, TypeItem { name, .. }) in indexed.items.iter_types() { + let Some(name) = name else { continue }; + let Some(kind) = checked.lookup_type(id) else { continue }; + let signature = pretty2::print_signature(engine, name, kind); + writeln!(snapshot, "{signature}").unwrap(); + } + + if !checked.synonyms.is_empty() { + writeln!(snapshot, "\nSynonyms").unwrap(); + } + for (id, TypeItem { name, .. }) in indexed.items.iter_types() { + let Some(name) = name else { continue }; + let Some(synonym) = checked.lookup_synonym(id) else { continue }; + let synonym_id = engine.intern_synonym(synonym); + let synonym_type = + engine.intern_type(checking2::core::Type::SynonymApplication(synonym_id)); + let synonym = pretty2::print(engine, synonym_type); + writeln!(snapshot, "{name} = {synonym}").unwrap(); + } + + if !checked.roles.is_empty() { + writeln!(snapshot, "\nRoles").unwrap(); + } + for (id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { + let (TypeItemKind::Data { .. } + | TypeItemKind::Newtype { .. } + | TypeItemKind::Foreign { .. }) = kind + else { + continue; + }; + let Some(name) = name else { continue }; + let Some(roles) = checked.lookup_roles(id) else { continue }; + let roles_str: Vec<_> = roles.iter().map(|r| format!("{r:?}")).collect(); + writeln!(snapshot, "{name} = [{}]", roles_str.join(", ")).unwrap(); + } + + if !checked.errors.is_empty() { + writeln!(snapshot, "\nErrors").unwrap(); + } + for error in &checked.errors { + writeln!(snapshot, "{error:#?}").unwrap(); + } + + snapshot +} + fn resolve_class_name( engine: &QueryEngine, indexed: &indexing::IndexedModule, diff --git a/tests-integration/src/lib.rs b/tests-integration/src/lib.rs index 795c2631..1308f8cf 100644 --- a/tests-integration/src/lib.rs +++ b/tests-integration/src/lib.rs @@ -40,7 +40,7 @@ pub fn load_compiler(folder: &Path) -> (QueryEngine, Files) { let mut files = Files::default(); prim::configure(&mut engine, &mut files); - if folder.starts_with("fixtures/checking/") { + if folder.starts_with("fixtures/checking/") || folder.starts_with("fixtures/checking2/") { let prelude = Path::new("fixtures/checking/prelude"); load_folder(prelude).for_each(|path| { load_file(&mut engine, &mut files, &path); diff --git a/tests-integration/tests/checking2.rs b/tests-integration/tests/checking2.rs new file mode 100644 index 00000000..45b89844 --- /dev/null +++ b/tests-integration/tests/checking2.rs @@ -0,0 +1,2 @@ +#[path = "checking2/generated.rs"] +mod generated; diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs new file mode 100644 index 00000000..46fdd574 --- /dev/null +++ b/tests-integration/tests/checking2/generated.rs @@ -0,0 +1,31 @@ +// Do not edit! See build.rs + +#[rustfmt::skip] +fn run_test(folder: &str, file: &str) { + let path = std::path::Path::new("fixtures/checking2").join(folder); + let (engine, _) = tests_integration::load_compiler(&path); + let Some(id) = engine.module_file(file) else { return }; + + let level = match std::env::var("TRACE_LEVEL").as_deref() { + Ok("debug") => tracing::Level::DEBUG, + _ => tracing::Level::WARN, + }; + + let target_dir = env!("CARGO_TARGET_TMPDIR"); + let test_name = format!("{}_{}", folder, file); + let (report, trace_path) = tests_integration::trace::with_file_trace( + level, + target_dir, + &test_name, + || tests_integration::generated::basic::report_checked2(&engine, id) + ); + + println!("trace: {}", trace_path.display()); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/checking2").join(folder)); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| insta::assert_snapshot!(file, report)); +} + +#[rustfmt::skip] #[test] fn test_gitkeep_main() { run_test("gitkeep", "Main"); } From 51300129d0d9d310b9e7e237b0540b23e2872407 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 23 Feb 2026 23:25:18 +0800 Subject: [PATCH 234/386] Add initial integration tests for foreign data --- .../fixtures/checking2/001_foreign_check/Main.purs | 5 +++++ .../fixtures/checking2/001_foreign_check/Main.snap | 11 +++++++++++ .../checking2/002_foreign_recursive/Main.purs | 6 ++++++ .../checking2/002_foreign_recursive/Main.snap | 11 +++++++++++ tests-integration/tests/checking2/generated.rs | 4 ++++ 5 files changed, 37 insertions(+) create mode 100644 tests-integration/fixtures/checking2/001_foreign_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/001_foreign_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/002_foreign_recursive/Main.purs create mode 100644 tests-integration/fixtures/checking2/002_foreign_recursive/Main.snap diff --git a/tests-integration/fixtures/checking2/001_foreign_check/Main.purs b/tests-integration/fixtures/checking2/001_foreign_check/Main.purs new file mode 100644 index 00000000..afb2e802 --- /dev/null +++ b/tests-integration/fixtures/checking2/001_foreign_check/Main.purs @@ -0,0 +1,5 @@ +module Main where + +foreign import data M :: Type -> Type +foreign import data P :: forall k. k -> Type +foreign import data T :: forall k. P k -> Type diff --git a/tests-integration/fixtures/checking2/001_foreign_check/Main.snap b/tests-integration/fixtures/checking2/001_foreign_check/Main.snap new file mode 100644 index 00000000..7ed97b71 --- /dev/null +++ b/tests-integration/fixtures/checking2/001_foreign_check/Main.snap @@ -0,0 +1,11 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +M :: Type -> Type +P :: forall (k :: Type). (k :: Type) -> Type +T :: forall (t2 :: Type) (k :: (t2 :: Type)). P @(t2 :: Type) (k :: (t2 :: Type)) -> Type diff --git a/tests-integration/fixtures/checking2/002_foreign_recursive/Main.purs b/tests-integration/fixtures/checking2/002_foreign_recursive/Main.purs new file mode 100644 index 00000000..96bb69ea --- /dev/null +++ b/tests-integration/fixtures/checking2/002_foreign_recursive/Main.purs @@ -0,0 +1,6 @@ +module Main where + +foreign import data T :: T -> Type + +foreign import data A :: B -> Type +foreign import data B :: A -> Type diff --git a/tests-integration/fixtures/checking2/002_foreign_recursive/Main.snap b/tests-integration/fixtures/checking2/002_foreign_recursive/Main.snap new file mode 100644 index 00000000..9c90d3d4 --- /dev/null +++ b/tests-integration/fixtures/checking2/002_foreign_recursive/Main.snap @@ -0,0 +1,11 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +T :: ?[invalid recursive type] +A :: ?[invalid recursive type] +B :: ?[invalid recursive type] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 46fdd574..3d711953 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -29,3 +29,7 @@ fn run_test(folder: &str, file: &str) { } #[rustfmt::skip] #[test] fn test_gitkeep_main() { run_test("gitkeep", "Main"); } + +#[rustfmt::skip] #[test] fn test_001_foreign_check_main() { run_test("001_foreign_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_002_foreign_recursive_main() { run_test("002_foreign_recursive", "Main"); } From 196567463a6220208a79405927779e76bd45f371 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 00:18:19 +0800 Subject: [PATCH 235/386] Use flags for compiler-scripts snapshot manipulation --- .claude/skills/type-checker-tests/SKILL.md | 9 +++ .../reference/compiler-scripts.md | 7 +- compiler-scripts/src/main.rs | 69 ++++++++++--------- compiler-scripts/src/test_runner.rs | 29 +++----- compiler-scripts/src/test_runner/cli.rs | 33 +++------ compiler-scripts/src/test_runner/nextest.rs | 3 +- compiler-scripts/src/test_runner/ui.rs | 8 ++- 7 files changed, 75 insertions(+), 83 deletions(-) diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.claude/skills/type-checker-tests/SKILL.md index 0a42f19a..f288a0c5 100644 --- a/.claude/skills/type-checker-tests/SKILL.md +++ b/.claude/skills/type-checker-tests/SKILL.md @@ -48,6 +48,15 @@ test' [x] = x just t checking NNN MMM ``` +### 4. Accept or reject snapshots + +```bash +just t checking NNN --diff # Inspect a fixture diff +just t checking NNN --accept # Accept a specific fixture +just t checking NNN --reject # Reject a specific fixture +just t checking --accept --confirm # Accept all pending snapshots +``` + ## Multi-File Tests For imports, re-exports, or cross-module behavior: diff --git a/.claude/skills/type-checker-tests/reference/compiler-scripts.md b/.claude/skills/type-checker-tests/reference/compiler-scripts.md index aac183a6..5e869f5f 100644 --- a/.claude/skills/type-checker-tests/reference/compiler-scripts.md +++ b/.claude/skills/type-checker-tests/reference/compiler-scripts.md @@ -28,11 +28,12 @@ just t --delete "name" # Dry-run fixture deletion (use --confirm) ### Snapshot commands ```bash -just t accept [--all] [filters...] # Accept pending snapshots -just t reject [--all] [filters...] # Reject pending snapshots +just t [filters...] --accept # Accept matching snapshots +just t [filters...] --reject # Reject matching snapshots +just t --accept --confirm # Accept all pending snapshots ``` -Requires `--all` flag when no filters provided (safety guardrail). +For safety, unfiltered `--accept` requires `--confirm`. ### Exclusion filters diff --git a/compiler-scripts/src/main.rs b/compiler-scripts/src/main.rs index d887731d..0d5669da 100644 --- a/compiler-scripts/src/main.rs +++ b/compiler-scripts/src/main.rs @@ -2,8 +2,8 @@ use clap::Parser; use console::style; use compiler_scripts::test_runner::{ - CategoryCommand, DeleteFixtureOutcome, RunArgs, TestCategory, accept_category, create_fixture, - delete_fixture, reject_category, run_category, + DeleteFixtureOutcome, RunArgs, TestCategory, accept_category, create_fixture, delete_fixture, + reject_category, run_category, }; #[derive(Parser)] @@ -78,42 +78,47 @@ fn main() { return; } - match &cli.args.command { - Some(CategoryCommand::Accept(args)) => { - let outcome = match accept_category(cli.category, args) { - Ok(outcome) => outcome, - Err(error) => { - eprintln!("{:#}", error); - std::process::exit(1); - } - }; - if !outcome.success { + if cli.args.accept && cli.args.reject { + eprintln!("--accept and --reject cannot be used together"); + std::process::exit(2); + } + + if cli.args.accept { + let outcome = match accept_category(cli.category, &cli.args.filters, cli.args.confirm) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); std::process::exit(1); } + }; + if !outcome.success { + std::process::exit(1); } - Some(CategoryCommand::Reject(args)) => { - let outcome = match reject_category(cli.category, args) { - Ok(outcome) => outcome, - Err(error) => { - eprintln!("{:#}", error); - std::process::exit(1); - } - }; - if !outcome.success { + return; + } + + if cli.args.reject { + let outcome = match reject_category(cli.category, &cli.args.filters) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); std::process::exit(1); } + }; + if !outcome.success { + std::process::exit(1); } - None => { - let outcome = match run_category(cli.category, &cli.args) { - Ok(outcome) => outcome, - Err(error) => { - eprintln!("{:#}", error); - std::process::exit(1); - } - }; - if !outcome.tests_passed { - std::process::exit(1); - } + return; + } + + let outcome = match run_category(cli.category, &cli.args) { + Ok(outcome) => outcome, + Err(error) => { + eprintln!("{:#}", error); + std::process::exit(1); } + }; + if !outcome.tests_passed { + std::process::exit(1); } } diff --git a/compiler-scripts/src/test_runner.rs b/compiler-scripts/src/test_runner.rs index 1e983413..a8bc3065 100644 --- a/compiler-scripts/src/test_runner.rs +++ b/compiler-scripts/src/test_runner.rs @@ -8,7 +8,7 @@ mod traces; mod ui; pub use category::TestCategory; -pub use cli::{CategoryCommand, RunArgs, SnapshotArgs}; +pub use cli::RunArgs; use std::path::PathBuf; use std::time::Instant; @@ -61,16 +61,17 @@ pub struct SnapshotOutcome { pub fn accept_category( category: TestCategory, - args: &SnapshotArgs, + filters: &[String], + confirm: bool, ) -> anyhow::Result { - let snapshots = pending::collect_pending_snapshots(category, &args.filters)?; + let snapshots = pending::collect_pending_snapshots(category, filters)?; if snapshots.is_empty() { println!("{}", style("No pending snapshots found.").dim()); return Ok(SnapshotOutcome { success: true, count: 0 }); } - if !args.all && args.filters.is_empty() { + if !confirm && filters.is_empty() { println!("{} pending snapshot(s) in {}", snapshots.len(), style(category.as_str()).cyan()); println!(); for info in &snapshots { @@ -79,7 +80,7 @@ pub fn accept_category( println!(); println!( "To accept all, run: {}", - style(format!("just t {} accept --all", category.as_str())).cyan() + style(format!("just t {} --accept --confirm", category.as_str())).cyan() ); return Ok(SnapshotOutcome { success: false, count: 0 }); } @@ -93,29 +94,15 @@ pub fn accept_category( pub fn reject_category( category: TestCategory, - args: &SnapshotArgs, + filters: &[String], ) -> anyhow::Result { - let snapshots = pending::collect_pending_snapshots(category, &args.filters)?; + let snapshots = pending::collect_pending_snapshots(category, filters)?; if snapshots.is_empty() { println!("{}", style("No pending snapshots found.").dim()); return Ok(SnapshotOutcome { success: true, count: 0 }); } - if !args.all && args.filters.is_empty() { - println!("{} pending snapshot(s) in {}", snapshots.len(), style(category.as_str()).cyan()); - println!(); - for info in &snapshots { - println!(" {}", style(&info.short_path).dim()); - } - println!(); - println!( - "To reject all, run: {}", - style(format!("just t {} reject --all", category.as_str())).cyan() - ); - return Ok(SnapshotOutcome { success: false, count: 0 }); - } - let result = pending::reject_snapshots(&snapshots); println!(); println!("{}", style(format!("Rejected {} snapshot(s)", result.rejected)).red()); diff --git a/compiler-scripts/src/test_runner/cli.rs b/compiler-scripts/src/test_runner/cli.rs index fae93f76..33d4972b 100644 --- a/compiler-scripts/src/test_runner/cli.rs +++ b/compiler-scripts/src/test_runner/cli.rs @@ -1,23 +1,4 @@ -use clap::{Args, Subcommand}; - -#[derive(Subcommand, Clone, Debug)] -pub enum CategoryCommand { - /// Accept pending snapshots for this category - Accept(SnapshotArgs), - /// Reject pending snapshots for this category - Reject(SnapshotArgs), -} - -#[derive(Args, Clone, Debug)] -pub struct SnapshotArgs { - /// Snapshot filters (same as test filters) - #[arg(num_args = 0..)] - pub filters: Vec, - - /// Accept/reject all pending snapshots (required when no filters provided) - #[arg(long)] - pub all: bool, -} +use clap::Args; #[derive(Args, Clone, Debug)] pub struct RunArgs { @@ -29,13 +10,17 @@ pub struct RunArgs { #[arg(long, value_name = "NAME")] pub delete: Option, - /// Confirm deletion for --delete + /// Confirm destructive/global actions (required for unfiltered --accept and --delete) #[arg(long)] pub confirm: bool, - /// Subcommand (accept/reject) or test filters - #[command(subcommand)] - pub command: Option, + /// Accept pending snapshots matching filters + #[arg(long)] + pub accept: bool, + + /// Reject pending snapshots matching filters + #[arg(long)] + pub reject: bool, /// Test name or number filters (passed through to nextest) #[arg(num_args = 0..)] diff --git a/compiler-scripts/src/test_runner/nextest.rs b/compiler-scripts/src/test_runner/nextest.rs index 94db0428..1ec07475 100644 --- a/compiler-scripts/src/test_runner/nextest.rs +++ b/compiler-scripts/src/test_runner/nextest.rs @@ -64,7 +64,8 @@ pub fn run_nextest( create: None, delete: None, confirm: false, - command: None, + accept: false, + reject: false, filters: args.filters.clone(), verbose: true, debug: args.debug, diff --git a/compiler-scripts/src/test_runner/ui.rs b/compiler-scripts/src/test_runner/ui.rs index 74a3c56d..0552f52e 100644 --- a/compiler-scripts/src/test_runner/ui.rs +++ b/compiler-scripts/src/test_runner/ui.rs @@ -245,8 +245,12 @@ fn render_pending( fn format_accept_reject_cmd(category_name: &str, filters_str: &str, action: &str) -> String { if filters_str.is_empty() { - format!("just t {} {} --all", category_name, action) + if action == "accept" { + format!("just t {} --accept --confirm", category_name) + } else { + format!("just t {} --reject", category_name) + } } else { - format!("just t {} {}{}", category_name, action, filters_str) + format!("just t {}{} --{}", category_name, filters_str, action) } } From a2a4c86b4f3758c0f8f372d849ebf5e8acc0e1ab Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 00:23:33 +0800 Subject: [PATCH 236/386] Move .claude to .agents and add symlinks to skills --- {.claude => .agents}/skills/type-checker-tests/SKILL.md | 0 .../skills/type-checker-tests/reference/compiler-scripts.md | 0 .claude/skills | 1 + 3 files changed, 1 insertion(+) rename {.claude => .agents}/skills/type-checker-tests/SKILL.md (100%) rename {.claude => .agents}/skills/type-checker-tests/reference/compiler-scripts.md (100%) create mode 120000 .claude/skills diff --git a/.claude/skills/type-checker-tests/SKILL.md b/.agents/skills/type-checker-tests/SKILL.md similarity index 100% rename from .claude/skills/type-checker-tests/SKILL.md rename to .agents/skills/type-checker-tests/SKILL.md diff --git a/.claude/skills/type-checker-tests/reference/compiler-scripts.md b/.agents/skills/type-checker-tests/reference/compiler-scripts.md similarity index 100% rename from .claude/skills/type-checker-tests/reference/compiler-scripts.md rename to .agents/skills/type-checker-tests/reference/compiler-scripts.md diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file From 84263ee982ffe37ca13b7d03612836ccb0fb0c72 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 00:29:29 +0800 Subject: [PATCH 237/386] Add initial tests for data declarations --- .../fixtures/checking2/003_data_check/Main.purs | 4 ++++ .../fixtures/checking2/003_data_check/Main.snap | 10 ++++++++++ .../fixtures/checking2/004_data_infer/Main.purs | 3 +++ .../fixtures/checking2/004_data_infer/Main.snap | 10 ++++++++++ tests-integration/tests/checking2/generated.rs | 4 ++++ 5 files changed, 31 insertions(+) create mode 100644 tests-integration/fixtures/checking2/003_data_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/003_data_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/004_data_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/004_data_infer/Main.snap diff --git a/tests-integration/fixtures/checking2/003_data_check/Main.purs b/tests-integration/fixtures/checking2/003_data_check/Main.purs new file mode 100644 index 00000000..95a8f1ea --- /dev/null +++ b/tests-integration/fixtures/checking2/003_data_check/Main.purs @@ -0,0 +1,4 @@ +module Main where + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy diff --git a/tests-integration/fixtures/checking2/003_data_check/Main.snap b/tests-integration/fixtures/checking2/003_data_check/Main.snap new file mode 100644 index 00000000..1df69728 --- /dev/null +++ b/tests-integration/fixtures/checking2/003_data_check/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type diff --git a/tests-integration/fixtures/checking2/004_data_infer/Main.purs b/tests-integration/fixtures/checking2/004_data_infer/Main.purs new file mode 100644 index 00000000..24cc27bb --- /dev/null +++ b/tests-integration/fixtures/checking2/004_data_infer/Main.purs @@ -0,0 +1,3 @@ +module Main where + +data Proxy a = Proxy diff --git a/tests-integration/fixtures/checking2/004_data_infer/Main.snap b/tests-integration/fixtures/checking2/004_data_infer/Main.snap new file mode 100644 index 00000000..9ce46748 --- /dev/null +++ b/tests-integration/fixtures/checking2/004_data_infer/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (t0 :: Type) (a :: (t0 :: Type)). Proxy @(t0 :: Type) (a :: (t0 :: Type)) + +Types +Proxy :: forall (t0 :: Type). (t0 :: Type) -> Type diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 3d711953..981468e2 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -33,3 +33,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_001_foreign_check_main() { run_test("001_foreign_check", "Main"); } #[rustfmt::skip] #[test] fn test_002_foreign_recursive_main() { run_test("002_foreign_recursive", "Main"); } + +#[rustfmt::skip] #[test] fn test_003_data_check_main() { run_test("003_data_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_004_data_infer_main() { run_test("004_data_infer", "Main"); } From 4d6be0d911f285f7129e9dceb1cbde08d8eb818f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 00:31:06 +0800 Subject: [PATCH 238/386] Add initial tests for newtype declarations --- .../fixtures/checking2/005_newtype_check/Main.purs | 4 ++++ .../fixtures/checking2/005_newtype_check/Main.snap | 12 ++++++++++++ .../fixtures/checking2/006_newtype_infer/Main.purs | 3 +++ .../fixtures/checking2/006_newtype_infer/Main.snap | 12 ++++++++++++ tests-integration/tests/checking2/generated.rs | 4 ++++ 5 files changed, 35 insertions(+) create mode 100644 tests-integration/fixtures/checking2/005_newtype_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/005_newtype_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/006_newtype_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/006_newtype_infer/Main.snap diff --git a/tests-integration/fixtures/checking2/005_newtype_check/Main.purs b/tests-integration/fixtures/checking2/005_newtype_check/Main.purs new file mode 100644 index 00000000..08652523 --- /dev/null +++ b/tests-integration/fixtures/checking2/005_newtype_check/Main.purs @@ -0,0 +1,4 @@ +module Main where + +newtype Tagged :: forall k. k -> Type -> Type +newtype Tagged t a = Tagged a diff --git a/tests-integration/fixtures/checking2/005_newtype_check/Main.snap b/tests-integration/fixtures/checking2/005_newtype_check/Main.snap new file mode 100644 index 00000000..4c4f77d8 --- /dev/null +++ b/tests-integration/fixtures/checking2/005_newtype_check/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tagged :: + forall (k :: Type) (t :: (k :: Type)) (a :: Type). + (a :: Type) -> Tagged @(k :: Type) (t :: (k :: Type)) (a :: Type) + +Types +Tagged :: forall (k :: Type). (k :: Type) -> Type -> Type diff --git a/tests-integration/fixtures/checking2/006_newtype_infer/Main.purs b/tests-integration/fixtures/checking2/006_newtype_infer/Main.purs new file mode 100644 index 00000000..e17c2b08 --- /dev/null +++ b/tests-integration/fixtures/checking2/006_newtype_infer/Main.purs @@ -0,0 +1,3 @@ +module Main where + +newtype Tagged t a = Tagged a diff --git a/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap new file mode 100644 index 00000000..af769f49 --- /dev/null +++ b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tagged :: + forall (t0 :: Type) (t :: (t0 :: Type)) (a :: Type). + (a :: Type) -> Tagged @(t0 :: Type) (t :: (t0 :: Type)) (a :: Type) + +Types +Tagged :: forall (t0 :: Type). (t0 :: Type) -> Type -> Type diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 981468e2..18b4dffb 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -37,3 +37,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_003_data_check_main() { run_test("003_data_check", "Main"); } #[rustfmt::skip] #[test] fn test_004_data_infer_main() { run_test("004_data_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_005_newtype_check_main() { run_test("005_newtype_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_006_newtype_infer_main() { run_test("006_newtype_infer", "Main"); } From 3d965b164571685ef5da806c88bc21d6f8737885 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 00:56:37 +0800 Subject: [PATCH 239/386] Add checking rule for synonym signatures --- compiler-core/checking2/src/source.rs | 71 +++++++++++++------ .../checking2/007_synonym_check/Main.purs | 4 ++ .../checking2/007_synonym_check/Main.snap | 9 +++ .../tests/checking2/generated.rs | 2 + 4 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 tests-integration/fixtures/checking2/007_synonym_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/007_synonym_check/Main.snap diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 5f4ee42e..c737475c 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -8,7 +8,7 @@ pub mod types; use building_types::QueryResult; use indexing::{TermItemId, TypeItemId}; use lowering::{ - DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, TermItemIr, TypeItemIr, + DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, TypeItemIr, TypeVariableBinding, }; use smol_str::SmolStr; @@ -161,17 +161,20 @@ where match item { TypeItemIr::DataGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_data_signature(state, context, item_id, *signature)?; + check_signature_type(state, context, item_id, *signature)?; } TypeItemIr::NewtypeGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_data_signature(state, context, item_id, *signature)?; + check_signature_type(state, context, item_id, *signature)?; + } + TypeItemIr::SynonymGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; } - TypeItemIr::SynonymGroup { .. } => todo!(), TypeItemIr::ClassGroup { .. } => todo!(), TypeItemIr::Foreign { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_foreign_signature(state, context, item_id, *signature)?; + check_signature_type(state, context, item_id, *signature)?; } TypeItemIr::Operator { .. } => todo!(), } @@ -179,7 +182,7 @@ where Ok(()) } -fn check_data_signature( +fn check_signature_type( state: &mut CheckState, context: &CheckContext, item_id: TypeItemId, @@ -188,22 +191,8 @@ fn check_data_signature( where Q: ExternalQueries, { - let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; - state.checked.types.insert(item_id, inferred_type); - Ok(()) -} - -fn check_foreign_signature( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, - signature: lowering::TypeId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let (inferred_type, _) = types::check_kind(state, context, signature, context.prim.t)?; - state.checked.types.insert(item_id, inferred_type); + let (checked_type, _) = types::check_kind(state, context, signature, context.prim.t)?; + state.checked.types.insert(item_id, checked_type); Ok(()) } @@ -229,7 +218,10 @@ where let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; check_data_equation(state, context, scc, item_id, *signature, variables)?; } - TypeItemIr::SynonymGroup { .. } => todo!(), + TypeItemIr::SynonymGroup { signature, synonym, .. } => { + let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; + check_synonym_equation(state, context, item_id, *signature, variables, *synonym)?; + } TypeItemIr::ClassGroup { .. } => todo!(), TypeItemIr::Foreign { .. } => {} TypeItemIr::Operator { .. } => todo!(), @@ -474,3 +466,36 @@ where Ok(()) } + +fn check_synonym_equation( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + signature: Option, + bindings: &[TypeVariableBinding], + synonym: Option, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (parameter, result) = if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + let signature = + signature::inspect_signature(state, context, (signature_id, signature_kind), bindings)?; + let parameters = + check_type_variable_bindings(state, context, bindings, &signature.arguments)?; + (parameters, signature.result) + } else { + todo!() + }; + + let synonym = if let Some(synonym) = synonym { + let (synonym, _) = types::check_kind(state, context, synonym, result)?; + synonym + } else { + context.unknown("invalid synonym type") + }; + + Ok(()) +} diff --git a/tests-integration/fixtures/checking2/007_synonym_check/Main.purs b/tests-integration/fixtures/checking2/007_synonym_check/Main.purs new file mode 100644 index 00000000..5622b4ae --- /dev/null +++ b/tests-integration/fixtures/checking2/007_synonym_check/Main.purs @@ -0,0 +1,4 @@ +module Main where + +type NonZeroInt :: Type +type NonZeroInt = Int diff --git a/tests-integration/fixtures/checking2/007_synonym_check/Main.snap b/tests-integration/fixtures/checking2/007_synonym_check/Main.snap new file mode 100644 index 00000000..b4bdd717 --- /dev/null +++ b/tests-integration/fixtures/checking2/007_synonym_check/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +NonZeroInt :: Type diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 18b4dffb..0aa5e98d 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -41,3 +41,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_005_newtype_check_main() { run_test("005_newtype_check", "Main"); } #[rustfmt::skip] #[test] fn test_006_newtype_infer_main() { run_test("006_newtype_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_007_synonym_check_main() { run_test("007_synonym_check", "Main"); } From e9326fd1b46c7ada0c2e90352f9b7e485a2698ec Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 14:06:24 +0800 Subject: [PATCH 240/386] Use unique for generating variable names --- compiler-core/checking2/src/core.rs | 12 +++++++++++- compiler-core/checking2/src/core/generalise.rs | 10 ++-------- .../fixtures/checking2/004_data_infer/Main.snap | 4 ++-- .../fixtures/checking2/006_newtype_infer/Main.snap | 6 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 498ea1f7..d28e3166 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use files::FileId; use indexing::TypeItemId; use itertools::Itertools; -use smol_str::SmolStr; +use smol_str::{SmolStr, SmolStrBuilder}; /// A globally unique identity for a rigid type variable. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -24,6 +24,16 @@ pub struct Name { pub unique: u32, } +impl Name { + /// Renders this name as a stable textual variable like `t42`. + pub fn as_text(self) -> SmolStr { + let mut text = SmolStrBuilder::new(); + text.push('t'); + text.push_str(&self.unique.to_string()); + text.finish() + } +} + /// A marker used to represent binding levels of variables. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Depth(pub u32); diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index e87906dd..d366653d 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -22,12 +22,10 @@ use building_types::QueryResult; use petgraph::algo; use petgraph::prelude::DiGraphMap; use rustc_hash::FxHashSet; -use smol_str::SmolStr; - -use crate::core::{ForallBinder, Type, TypeId, normalise, zonk}; use crate::ExternalQueries; use crate::context::CheckContext; +use crate::core::{ForallBinder, Type, TypeId, normalise, zonk}; use crate::state::{CheckState, UnificationEntry}; type UniGraph = DiGraphMap; @@ -126,10 +124,6 @@ where aux(graph, state, context, id, None, &mut visited_kinds) } -fn generate_type_name(id: u32) -> SmolStr { - SmolStr::new(format!("t{id}")) -} - /// Generalises a given type. See also module-level documentation. pub fn generalise( state: &mut CheckState, @@ -177,8 +171,8 @@ where for &unification_id in unsolved.iter() { let UnificationEntry { kind, .. } = *state.unifications.get(unification_id); - let text = generate_type_name(unification_id); let name = state.names.fresh(); + let text = name.as_text(); let binder = ForallBinder { visible: false, name, text, kind }; let binder = context.intern_forall_binder(binder); diff --git a/tests-integration/fixtures/checking2/004_data_infer/Main.snap b/tests-integration/fixtures/checking2/004_data_infer/Main.snap index 9ce46748..a2931b9d 100644 --- a/tests-integration/fixtures/checking2/004_data_infer/Main.snap +++ b/tests-integration/fixtures/checking2/004_data_infer/Main.snap @@ -4,7 +4,7 @@ assertion_line: 28 expression: report --- Terms -Proxy :: forall (t0 :: Type) (a :: (t0 :: Type)). Proxy @(t0 :: Type) (a :: (t0 :: Type)) +Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 :: Type)) Types -Proxy :: forall (t0 :: Type). (t0 :: Type) -> Type +Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type diff --git a/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap index af769f49..8b44e1d8 100644 --- a/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap +++ b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap @@ -5,8 +5,8 @@ expression: report --- Terms Tagged :: - forall (t0 :: Type) (t :: (t0 :: Type)) (a :: Type). - (a :: Type) -> Tagged @(t0 :: Type) (t :: (t0 :: Type)) (a :: Type) + forall (t2 :: Type) (t :: (t2 :: Type)) (a :: Type). + (a :: Type) -> Tagged @(t2 :: Type) (t :: (t2 :: Type)) (a :: Type) Types -Tagged :: forall (t0 :: Type). (t0 :: Type) -> Type -> Type +Tagged :: forall (t2 :: Type). (t2 :: Type) -> Type -> Type From 80cdc2321f8cae8a4e20072462452d24ba9b5805 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:13:15 +0800 Subject: [PATCH 241/386] Implement builder-style API for printer --- compiler-core/checking2/src/core/pretty.rs | 120 +++++++-------------- compiler-core/checking2/src/state.rs | 2 +- tests-integration/src/generated/basic.rs | 6 +- 3 files changed, 45 insertions(+), 83 deletions(-) diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 170036b6..8f74dd87 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -14,68 +14,50 @@ use crate::core::{ type Doc<'a> = DocBuilder<'a, Arena<'a>, ()>; -pub struct PrettyConfig { - pub width: usize, +pub struct Pretty<'a, Q> { + queries: &'a Q, + width: usize, + signature: Option<&'a str>, + names: FxHashMap, } -impl Default for PrettyConfig { - fn default() -> PrettyConfig { - PrettyConfig { width: 100 } +impl<'a, Q: ExternalQueries> Pretty<'a, Q> { + pub fn new(queries: &'a Q) -> Self { + Pretty { queries, width: 100, signature: None, names: FxHashMap::default() } } -} -pub fn print(queries: &Q, id: TypeId) -> SmolStr -where - Q: ExternalQueries, -{ - render(queries, &PrettyConfig::default(), |printer| printer.traverse(Precedence::Top, id)) -} - -pub fn print_with_config(queries: &Q, id: TypeId, config: &PrettyConfig) -> SmolStr -where - Q: ExternalQueries, -{ - render(queries, config, |printer| printer.traverse(Precedence::Top, id)) -} + pub fn width(mut self, width: usize) -> Self { + self.width = width; + self + } -pub fn print_signature(queries: &Q, name: &str, id: TypeId) -> SmolStr -where - Q: ExternalQueries, -{ - render(queries, &PrettyConfig::default(), |printer| printer.signature(name, id)) -} + pub fn signature(mut self, name: &'a str) -> Self { + self.signature = Some(name); + self + } -pub fn print_signature_with_config( - queries: &Q, - name: &str, - id: TypeId, - config: &PrettyConfig, -) -> SmolStr -where - Q: ExternalQueries, -{ - render(queries, config, |printer| printer.signature(name, id)) -} + pub fn names(mut self, names: impl IntoIterator) -> Self { + self.names.extend(names); + self + } -fn render( - queries: &Q, - config: &PrettyConfig, - f: impl for<'a> FnOnce(&mut Printer<'a, Q>) -> Doc<'a>, -) -> SmolStr -where - Q: ExternalQueries, -{ - let arena = Arena::new(); - let traversal = TraversalContext::new(); - let mut printer = Printer::new(&arena, queries, traversal); + pub fn render(self, id: TypeId) -> SmolStr { + let arena = Arena::new(); + let mut printer = Printer::new(&arena, self.queries); + printer.names = self.names; - let document = f(&mut printer); - let mut output = SmolStrBuilder::new(); - document - .render_fmt(config.width, &mut output) - .expect("critical failure: failed to render type"); + let document = if let Some(name) = self.signature { + printer.signature(name, id) + } else { + printer.traverse(Precedence::Top, id) + }; - output.finish() + let mut output = SmolStrBuilder::new(); + document + .render_fmt(self.width, &mut output) + .expect("critical failure: failed to render type"); + output.finish() + } } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -87,42 +69,21 @@ enum Precedence { Atom, } -struct TraversalContext { - names: FxHashMap, - next: u32, -} - -impl TraversalContext { - fn new() -> TraversalContext { - TraversalContext { names: FxHashMap::default(), next: 0 } - } - - fn render_name(&mut self, name: Name) -> String { - let unique = &mut self.next; - let name = self.names.entry(name).or_insert_with(|| { - let index = *unique; - *unique += 1; - format!("t{index}") - }); - String::clone(name) - } -} - struct Printer<'a, Q> where Q: ExternalQueries, { arena: &'a Arena<'a>, queries: &'a Q, - traversal: TraversalContext, + names: FxHashMap, } impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { - fn new(arena: &'a Arena<'a>, queries: &'a Q, traversal: TraversalContext) -> Printer<'a, Q> { - Printer { arena, queries, traversal } + fn new(arena: &'a Arena<'a>, queries: &'a Q) -> Printer<'a, Q> { + Printer { arena, queries, names: FxHashMap::default() } } fn lookup_type(&self, id: TypeId) -> Type { @@ -278,7 +239,8 @@ where } Type::Rigid(name, _, kind) => { - let name = self.traversal.render_name(name); + let name = self.names.entry(name).or_insert_with(|| name.as_text()); + let name = SmolStr::clone(name); let kind = self.traverse(Precedence::Top, kind); self.arena.text(format!("({name} :: ")).append(kind).append(self.arena.text(")")) } @@ -399,7 +361,7 @@ where // Register source-level names so rigid variables in the body // display their original names instead of synthetic ones. for binder in &binders { - self.traversal.names.insert(binder.name, binder.text.to_string()); + self.names.insert(binder.name, SmolStr::clone(&binder.text)); } let binders = binders diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index cf8c9d5f..6704859a 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -196,7 +196,7 @@ impl CheckState { Q: ExternalQueries, { let id = zonk::zonk(self, context, id)?; - let pretty = pretty::print(context.queries, id); + let pretty = pretty::Pretty::new(context.queries).render(id); Ok(context.queries.intern_smol_str(pretty)) } } diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index bb278bc4..bcdb7db7 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -464,7 +464,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TermItem { name, .. }) in indexed.items.iter_terms() { let Some(name) = name else { continue }; let Some(kind) = checked.lookup_term(id) else { continue }; - let signature = pretty2::print_signature(engine, name, kind); + let signature = pretty2::Pretty::new(engine).signature(name).render(kind); writeln!(snapshot, "{signature}").unwrap(); } @@ -472,7 +472,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(kind) = checked.lookup_type(id) else { continue }; - let signature = pretty2::print_signature(engine, name, kind); + let signature = pretty2::Pretty::new(engine).signature(name).render(kind); writeln!(snapshot, "{signature}").unwrap(); } @@ -485,7 +485,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { let synonym_id = engine.intern_synonym(synonym); let synonym_type = engine.intern_type(checking2::core::Type::SynonymApplication(synonym_id)); - let synonym = pretty2::print(engine, synonym_type); + let synonym = pretty2::Pretty::new(engine).render(synonym_type); writeln!(snapshot, "{name} = {synonym}").unwrap(); } From 95b526b61e8ebdd5620c5397534c7e50fe26f90a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 03:39:12 +0800 Subject: [PATCH 242/386] Implement checking and inference for synonyms --- compiler-core/checking2/src/core.rs | 6 ++ compiler-core/checking2/src/lib.rs | 7 +- compiler-core/checking2/src/source.rs | 91 ++++++++++++++++--- compiler-core/checking2/src/source/synonym.rs | 65 ++++--------- compiler-core/checking2/src/source/types.rs | 2 +- .../checking2/007_synonym_check/Main.purs | 6 ++ .../checking2/007_synonym_check/Main.snap | 7 ++ .../checking2/008_synonym_infer/Main.purs | 7 ++ .../checking2/008_synonym_infer/Main.snap | 16 ++++ tests-integration/src/generated/basic.rs | 15 +-- .../tests/checking2/generated.rs | 2 + 11 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 tests-integration/fixtures/checking2/008_synonym_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/008_synonym_infer/Main.snap diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index d28e3166..bb2811af 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -103,6 +103,12 @@ pub struct Synonym { pub arguments: Arc<[TypeId]>, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedSynonym { + pub parameters: Vec, + pub replacement: TypeId, +} + /// The core type representation used by the checker after name resolution. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Type { diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index d8b9422e..d9916298 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -19,7 +19,8 @@ use rustc_hash::FxHashMap; use smol_str::SmolStr; use crate::core::{ - ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, SynonymId, Type, TypeId, + CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, SynonymId, + Type, TypeId, }; use crate::error::CheckError; @@ -57,7 +58,7 @@ pub trait ExternalQueries: pub struct CheckedModule { pub types: FxHashMap, pub terms: FxHashMap, - pub synonyms: FxHashMap, + pub synonyms: FxHashMap, pub roles: FxHashMap>, pub errors: Vec, } @@ -71,7 +72,7 @@ impl CheckedModule { self.terms.get(&id).copied() } - pub fn lookup_synonym(&self, id: TypeItemId) -> Option { + pub fn lookup_synonym(&self, id: TypeItemId) -> Option { self.synonyms.get(&id).cloned() } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index c737475c..28456720 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -5,6 +5,8 @@ pub mod synonym; pub mod terms; pub mod types; +use std::mem; + use building_types::QueryResult; use indexing::{TermItemId, TypeItemId}; use lowering::{ @@ -15,7 +17,9 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, Type, TypeId, generalise, toolkit, unification, zonk}; +use crate::core::{ + CheckedSynonym, ForallBinder, Type, TypeId, generalise, toolkit, unification, zonk, +}; use crate::error::ErrorCrumb; use crate::state::CheckState; @@ -24,9 +28,15 @@ struct PendingDataType { constructors: Vec<(TermItemId, Vec)>, } +struct PendingSynonymType { + parameters: Vec, + replacement: TypeId, +} + #[derive(Default)] struct TypeSccState { data: Vec<(TypeItemId, PendingDataType)>, + synonym: Vec<(TypeItemId, PendingSynonymType)>, } /// Checks all type items in topological order. @@ -55,14 +65,15 @@ where prepare_binding_group(state, context, &items); } - let mut scc_state = TypeSccState { data: vec![] }; + let mut scc_state = TypeSccState::default(); for &item in &items { check_type_equation(state, context, &mut scc_state, item)?; } finalise_binding_group(state, context, &items)?; - finalise_data_constructors(state, context, scc_state)?; + finalise_data_constructors(state, context, &mut scc_state)?; + finalise_synonym_replacements(state, context, &mut scc_state)?; } Ok(()) } @@ -220,7 +231,7 @@ where } TypeItemIr::SynonymGroup { signature, synonym, .. } => { let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; - check_synonym_equation(state, context, item_id, *signature, variables, *synonym)?; + check_synonym_equation(state, context, scc, item_id, *signature, variables, *synonym)?; } TypeItemIr::ClassGroup { .. } => todo!(), TypeItemIr::Foreign { .. } => {} @@ -385,12 +396,12 @@ where fn finalise_data_constructors( state: &mut CheckState, context: &CheckContext, - scc: TypeSccState, + scc: &mut TypeSccState, ) -> QueryResult<()> where Q: ExternalQueries, { - for (item_id, PendingDataType { parameters, constructors }) in scc.data { + for (item_id, PendingDataType { parameters, constructors }) in mem::take(&mut scc.data) { // constructor_kind should have already been generalised by the // finalise_binding_group function. the kind signature is used // as the source of truth for constructing the kind applications @@ -470,6 +481,7 @@ where fn check_synonym_equation( state: &mut CheckState, context: &CheckContext, + scc: &mut TypeSccState, item_id: TypeItemId, signature: Option, bindings: &[TypeVariableBinding], @@ -478,24 +490,75 @@ fn check_synonym_equation( where Q: ExternalQueries, { - let (parameter, result) = if let Some(signature_id) = signature + let (parameters, result) = if let Some(signature_id) = signature && let Some(signature_kind) = state.checked.lookup_type(item_id) { - let signature = - signature::inspect_signature(state, context, (signature_id, signature_kind), bindings)?; - let parameters = - check_type_variable_bindings(state, context, bindings, &signature.arguments)?; - (parameters, signature.result) + check_synonym_equation_check(state, context, bindings, (signature_id, signature_kind))? } else { - todo!() + check_synonym_equation_infer(state, context, item_id, bindings)? }; - let synonym = if let Some(synonym) = synonym { + let replacement = if let Some(synonym) = synonym { let (synonym, _) = types::check_kind(state, context, synonym, result)?; synonym } else { context.unknown("invalid synonym type") }; + scc.synonym.push((item_id, PendingSynonymType { parameters, replacement })); + + Ok(()) +} + +fn check_synonym_equation_check( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + signature: (lowering::TypeId, TypeId), +) -> QueryResult<(Vec, TypeId)> +where + Q: ExternalQueries, +{ + let signature = signature::inspect_signature(state, context, signature, bindings)?; + let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; + Ok((parameters, signature.result)) +} + +fn check_synonym_equation_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + bindings: &[TypeVariableBinding], +) -> QueryResult<(Vec, TypeId)> +where + Q: ExternalQueries, +{ + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.iter().map(|binder| binder.kind); + let result = state.fresh_unification(context.queries, context.prim.t); + let inferred = context.intern_function_chain(kinds, result); + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; + } else { + state.checked.types.insert(item_id, inferred); + } + + Ok((bindings, result)) +} + +fn finalise_synonym_replacements( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, PendingSynonymType { parameters, replacement }) in mem::take(&mut scc.synonym) { + let replacement = zonk::zonk(state, context, replacement)?; + let checked_synonym = CheckedSynonym { parameters, replacement }; + state.checked.synonyms.insert(item_id, checked_synonym); + } Ok(()) } diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 8cef5c35..e86e4e9f 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -9,18 +9,17 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Saturation, Synonym, Type, TypeId, normalise, unification}; +use crate::core::{CheckedSynonym, Saturation, Synonym, Type, TypeId, normalise, unification}; use crate::error::ErrorKind; +use crate::source::types; use crate::state::CheckState; -use super::types; - pub fn lookup_file_synonym( state: &mut CheckState, context: &CheckContext, file_id: FileId, type_id: TypeItemId, -) -> QueryResult> +) -> QueryResult> where Q: ExternalQueries, { @@ -38,7 +37,7 @@ pub fn parse_synonym_application( state: &mut CheckState, context: &CheckContext, function: lowering::TypeId, -) -> QueryResult> +) -> QueryResult> where Q: ExternalQueries, { @@ -52,17 +51,19 @@ where return Ok(None); }; - let Some((synonym, kind)) = lookup_file_synonym(state, context, file_id, type_id)? else { + let Some((checked_synonym, kind)) = lookup_file_synonym(state, context, file_id, type_id)? + else { return Ok(None); }; - Ok(Some((file_id, type_id, synonym, kind))) + let arity = checked_synonym.parameters.len(); + Ok(Some((file_id, type_id, kind, arity))) } pub fn infer_synonym_constructor( state: &mut CheckState, context: &CheckContext, - (file_id, item_id, mut synonym, kind): (FileId, TypeItemId, Synonym, TypeId), + (file_id, item_id, kind, arity): (FileId, TypeItemId, TypeId, usize), id: lowering::TypeId, ) -> QueryResult<(TypeId, TypeId)> where @@ -72,15 +73,17 @@ where state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); } - let expected_arity = expected_synonym_arity(context, file_id, item_id)?; - if expected_arity > 0 { + if arity > 0 { state.insert_error(ErrorKind::PartialSynonymApplication { id }); let unknown = context.unknown("partial synonym application"); return Ok((unknown, unknown)); } - synonym.saturation = Saturation::Full; - synonym.arguments = Arc::default(); + let synonym = Synonym { + saturation: Saturation::Full, + reference: (file_id, item_id), + arguments: Arc::default(), + }; let synonym_id = context.intern_synonym(synonym); let synonym_type = context.intern_synonym_application(synonym_id); @@ -92,7 +95,7 @@ pub fn infer_synonym_application( state: &mut CheckState, context: &CheckContext, id: lowering::TypeId, - (file_id, type_id, synonym, function_kind): (FileId, TypeItemId, Synonym, TypeId), + (file_id, type_id, function_kind, arity): (FileId, TypeItemId, TypeId, usize), arguments: &[lowering::TypeId], ) -> QueryResult<(TypeId, TypeId)> where @@ -102,16 +105,13 @@ where state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id: type_id }); } - let expected_arity = expected_synonym_arity(context, file_id, type_id)?; - let actual_arity = arguments.len(); - - if actual_arity < expected_arity { + if arguments.len() < arity { state.insert_error(ErrorKind::PartialSynonymApplication { id }); let unknown = context.unknown("partial synonym application"); return Ok((unknown, unknown)); } - let (synonym_arguments, excess_arguments) = arguments.split_at(expected_arity); + let (synonym_arguments, excess_arguments) = arguments.split_at(arity); let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); let (argument_types, (_, synonym_kind)) = infer_synonym_application_chain( @@ -123,7 +123,7 @@ where let synonym = Synonym { saturation: Saturation::Full, - reference: synonym.reference, + reference: (file_id, type_id), arguments: Arc::from(argument_types), }; @@ -230,33 +230,6 @@ where } } -fn expected_synonym_arity( - context: &CheckContext, - file_id: FileId, - type_id: TypeItemId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let expected_arity = |lowered: &lowering::LoweredModule| { - let Some(lowering::TypeItemIr::SynonymGroup { synonym, .. }) = - lowered.info.get_type_item(type_id) - else { - return 0; - }; - let Some(synonym) = synonym else { - return 0; - }; - synonym.variables.len() - }; - if file_id == context.id { - Ok(expected_arity(&context.lowered)) - } else { - let lowered = context.queries.lowered(file_id)?; - Ok(expected_arity(&lowered)) - } -} - fn is_recursive_synonym( context: &CheckContext, file_id: FileId, diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 4e8b005d..2742c196 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -151,7 +151,7 @@ where if let Some((synonym, kind)) = synonym::lookup_file_synonym(state, context, file_id, type_id)? { - let synonym = (file_id, type_id, synonym, kind); + let synonym = (file_id, type_id, kind, synonym.parameters.len()); return synonym::infer_synonym_constructor(state, context, synonym, id); } let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); diff --git a/tests-integration/fixtures/checking2/007_synonym_check/Main.purs b/tests-integration/fixtures/checking2/007_synonym_check/Main.purs index 5622b4ae..af76a778 100644 --- a/tests-integration/fixtures/checking2/007_synonym_check/Main.purs +++ b/tests-integration/fixtures/checking2/007_synonym_check/Main.purs @@ -2,3 +2,9 @@ module Main where type NonZeroInt :: Type type NonZeroInt = Int + +type Identity :: Type -> Type +type Identity a = a + +type FourtyTwo :: Int +type FourtyTwo = 42 diff --git a/tests-integration/fixtures/checking2/007_synonym_check/Main.snap b/tests-integration/fixtures/checking2/007_synonym_check/Main.snap index b4bdd717..294c8deb 100644 --- a/tests-integration/fixtures/checking2/007_synonym_check/Main.snap +++ b/tests-integration/fixtures/checking2/007_synonym_check/Main.snap @@ -7,3 +7,10 @@ Terms Types NonZeroInt :: Type +Identity :: Type -> Type +FourtyTwo :: Int + +Synonyms +type NonZeroInt = Int +type Identity a = (a :: Type) +type FourtyTwo = 42 diff --git a/tests-integration/fixtures/checking2/008_synonym_infer/Main.purs b/tests-integration/fixtures/checking2/008_synonym_infer/Main.purs new file mode 100644 index 00000000..d41ba126 --- /dev/null +++ b/tests-integration/fixtures/checking2/008_synonym_infer/Main.purs @@ -0,0 +1,7 @@ +module Main where + +type NonZeroInt = Int + +type Identity a = a + +type FourtyTwo = 42 diff --git a/tests-integration/fixtures/checking2/008_synonym_infer/Main.snap b/tests-integration/fixtures/checking2/008_synonym_infer/Main.snap new file mode 100644 index 00000000..0e13593a --- /dev/null +++ b/tests-integration/fixtures/checking2/008_synonym_infer/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +NonZeroInt :: Type +Identity :: forall (t1 :: Type). (t1 :: Type) -> (t1 :: Type) +FourtyTwo :: Int + +Synonyms +type NonZeroInt = Int +type Identity a = (a :: (t1 :: Type)) +type FourtyTwo = 42 diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index bcdb7db7..9e6a74f5 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,11 +2,11 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; -use checking2::ExternalQueries; use checking2::core::pretty as pretty2; use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; use indexing::{ImportKind, TermItem, TypeItem, TypeItemId, TypeItemKind}; +use itertools::Itertools; use lowering::{ ExpressionKind, GraphNode, ImplicitTypeVariable, TermVariableResolution, TypeKind, TypeVariableResolution, @@ -481,12 +481,13 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { } for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; - let Some(synonym) = checked.lookup_synonym(id) else { continue }; - let synonym_id = engine.intern_synonym(synonym); - let synonym_type = - engine.intern_type(checking2::core::Type::SynonymApplication(synonym_id)); - let synonym = pretty2::Pretty::new(engine).render(synonym_type); - writeln!(snapshot, "{name} = {synonym}").unwrap(); + let Some(definition) = checked.lookup_synonym(id) else { continue }; + let names = definition.parameters.iter().map(|b| (b.name, b.text.clone())); + let replacement = pretty2::Pretty::new(engine).names(names).render(definition.replacement); + let binders = definition.parameters.iter().map(|b| b.text.as_str()).collect_vec(); + let binders_formatted = + if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; + writeln!(snapshot, "type {name}{binders_formatted} = {replacement}").unwrap(); } if !checked.roles.is_empty() { diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 0aa5e98d..25dfdc7c 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -43,3 +43,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_006_newtype_infer_main() { run_test("006_newtype_infer", "Main"); } #[rustfmt::skip] #[test] fn test_007_synonym_check_main() { run_test("007_synonym_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_008_synonym_infer_main() { run_test("008_synonym_infer", "Main"); } From 8c1f8284c1c92169fb4153c3db636dd1a4029c42 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:28:59 +0800 Subject: [PATCH 243/386] Apply clippy fixes --- compiler-compatibility/command/src/compat.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index c89baa2e..0d231c50 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -414,11 +414,10 @@ fn find_package_dir(packages_dir: &Path, package_name: &str) -> Option().is_ok() && entry.file_type().ok()?.is_dir() { + if let Some(suffix) = name_str.strip_prefix(&prefix) + && suffix.parse::().is_ok() && entry.file_type().ok()?.is_dir() { return entry.path().canonicalize().ok(); } - } } None } From d6f0eae9fc3d84f19da28440d6a195ab8fff23db Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:45:55 +0800 Subject: [PATCH 244/386] Add CheckedClass type and extend CheckedModule with classes map --- compiler-core/checking2/src/core.rs | 28 +++++++++++++++++++++++++++- compiler-core/checking2/src/lib.rs | 9 +++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index bb2811af..6260d666 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -13,7 +13,7 @@ pub mod zonk; use std::sync::Arc; use files::FileId; -use indexing::TypeItemId; +use indexing::{TermItemId, TypeItemId}; use itertools::Itertools; use smol_str::{SmolStr, SmolStrBuilder}; @@ -109,6 +109,32 @@ pub struct CheckedSynonym { pub replacement: TypeId, } +/// Represents a checked class declaration. +/// +/// Member types are stored in [`CheckedModule::terms`] quantified and +/// constrained with [`Type::Forall`] and [`Type::Constrained`]: +/// +/// ```purescript +/// eq :: forall a. Eq a => a -> a -> Boolean +/// ``` +/// +/// [`CheckedModule::terms`]: crate::CheckedModule::terms +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedClass { + /// Post-generalisation kind variable binders. + pub kind_binders: Vec, + /// Post-generalisation type parameter binders. + pub type_parameters: Vec, + /// Canonical class head, e.g. `Eq a` or `Foo @k a`. + pub canonical: TypeId, + /// Superclass constraints expressed in terms of the class head's rigids. + pub superclasses: Vec, + /// Functional dependencies, carried from lowering. + pub functional_dependencies: Arc<[lowering::FunctionalDependency]>, + /// Class member term item IDs. + pub members: Vec, +} + /// The core type representation used by the checker after name resolution. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Type { diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index d9916298..65210c72 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -19,8 +19,8 @@ use rustc_hash::FxHashMap; use smol_str::SmolStr; use crate::core::{ - CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, SynonymId, - Type, TypeId, + CheckedClass, CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, + SynonymId, Type, TypeId, }; use crate::error::CheckError; @@ -59,6 +59,7 @@ pub struct CheckedModule { pub types: FxHashMap, pub terms: FxHashMap, pub synonyms: FxHashMap, + pub classes: FxHashMap, pub roles: FxHashMap>, pub errors: Vec, } @@ -76,6 +77,10 @@ impl CheckedModule { self.synonyms.get(&id).cloned() } + pub fn lookup_class(&self, id: TypeItemId) -> Option { + self.classes.get(&id).cloned() + } + pub fn lookup_roles(&self, id: TypeItemId) -> Option> { self.roles.get(&id).cloned() } From 863e5bbaa8192e1fde51b252d15a40339a8ea907 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:45:59 +0800 Subject: [PATCH 245/386] Implement ClassGroup checking in source.rs --- compiler-core/checking2/src/source.rs | 235 +++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 5 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 28456720..0ce861fd 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -6,19 +6,22 @@ pub mod terms; pub mod types; use std::mem; +use std::sync::Arc; use building_types::QueryResult; use indexing::{TermItemId, TypeItemId}; +use itertools::Itertools; use lowering::{ - DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, TypeItemIr, - TypeVariableBinding, + ClassIr, DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, + TypeItemIr, TypeVariableBinding, }; use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{ - CheckedSynonym, ForallBinder, Type, TypeId, generalise, toolkit, unification, zonk, + CheckedClass, CheckedSynonym, ForallBinder, Type, TypeId, generalise, toolkit, unification, + zonk, }; use crate::error::ErrorCrumb; use crate::state::CheckState; @@ -33,10 +36,18 @@ struct PendingSynonymType { replacement: TypeId, } +struct PendingClassType { + parameters: Vec, + superclasses: Vec, + functional_dependencies: std::sync::Arc<[lowering::FunctionalDependency]>, + members: Vec<(TermItemId, TypeId)>, +} + #[derive(Default)] struct TypeSccState { data: Vec<(TypeItemId, PendingDataType)>, synonym: Vec<(TypeItemId, PendingSynonymType)>, + class: Vec<(TypeItemId, PendingClassType)>, } /// Checks all type items in topological order. @@ -74,6 +85,7 @@ where finalise_binding_group(state, context, &items)?; finalise_data_constructors(state, context, &mut scc_state)?; finalise_synonym_replacements(state, context, &mut scc_state)?; + finalise_classes(state, context, &mut scc_state)?; } Ok(()) } @@ -182,7 +194,10 @@ where let Some(signature) = signature else { return Ok(()) }; check_signature_type(state, context, item_id, *signature)?; } - TypeItemIr::ClassGroup { .. } => todo!(), + TypeItemIr::ClassGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; + } TypeItemIr::Foreign { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; check_signature_type(state, context, item_id, *signature)?; @@ -233,7 +248,10 @@ where let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; check_synonym_equation(state, context, scc, item_id, *signature, variables, *synonym)?; } - TypeItemIr::ClassGroup { .. } => todo!(), + TypeItemIr::ClassGroup { signature, class } => { + let Some(class) = class else { return Ok(()) }; + check_class_equation(state, context, scc, item_id, *signature, class)?; + } TypeItemIr::Foreign { .. } => {} TypeItemIr::Operator { .. } => todo!(), } @@ -562,3 +580,210 @@ where } Ok(()) } + +fn check_class_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + signature: Option, + class: &ClassIr, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let ClassIr { constraints, variables, functional_dependencies } = class; + + let parameters = if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + check_class_equation_check(state, context, variables, (signature_id, signature_kind))? + } else { + check_class_equation_infer(state, context, item_id, variables)? + }; + + let mut superclasses = vec![]; + for &constraint in constraints.iter() { + let (superclass, _) = + types::check_kind(state, context, constraint, context.prim.constraint)?; + superclasses.push(superclass); + } + + let functional_dependencies = Arc::clone(functional_dependencies); + let members = check_class_members(state, context, item_id)?; + + scc.class.push(( + item_id, + PendingClassType { parameters, superclasses, functional_dependencies, members }, + )); + + Ok(()) +} + +fn check_class_equation_check( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + signature: (lowering::TypeId, TypeId), +) -> QueryResult> +where + Q: ExternalQueries, +{ + let signature = signature::inspect_signature(state, context, signature, bindings)?; + check_type_variable_bindings(state, context, bindings, &signature.arguments) +} + +fn check_class_equation_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + bindings: &[TypeVariableBinding], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.iter().map(|binder| binder.kind); + let inferred = context.intern_function_chain(kinds, context.prim.constraint); + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; + } else { + state.checked.types.insert(item_id, inferred); + } + + Ok(bindings) +} + +fn check_class_members( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut members = vec![]; + + for member_id in context.indexed.pairs.class_members(item_id) { + let Some(TermItemIr::ClassMember { signature }) = + context.lowered.info.get_term_item(member_id) + else { + continue; + }; + + let Some(signature_id) = signature else { continue }; + + let (member_type, _) = types::check_kind(state, context, *signature_id, context.prim.t)?; + members.push((member_id, member_type)); + } + + Ok(members) +} + +fn finalise_classes( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, pending) in mem::take(&mut scc.class) { + let PendingClassType { parameters, superclasses, functional_dependencies, members } = + pending; + + let Some(class_kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let toolkit::InspectQuantified { binders: class_binders, quantified: class_inner } = + toolkit::inspect_quantified(state, context, class_kind)?; + + let toolkit::InspectFunction { arguments: class_parameters, .. } = + toolkit::inspect_function(state, context, class_inner)?; + + let get_parameter_kind = |index: usize| { + if let Some(kind) = class_parameters.get(index) { + *kind + } else { + context.unknown("invalid kind") + } + }; + + let kind_binders = class_binders + .iter() + .cloned() + .map(|binder| context.intern_forall_binder(binder)) + .collect_vec(); + + let type_parameters = parameters + .iter() + .cloned() + .enumerate() + .map(|(index, parameter)| { + let kind = get_parameter_kind(index); + let binder = ForallBinder { kind, ..parameter }; + context.intern_forall_binder(binder) + }) + .collect_vec(); + + let mut canonical = context.queries.intern_type(Type::Constructor(context.id, item_id)); + + for binder in &class_binders { + let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); + canonical = context.intern_kind_application(canonical, rigid); + } + + for (index, parameter) in parameters.iter().enumerate() { + let kind = get_parameter_kind(index); + let rigid = context.intern_rigid(parameter.name, state.depth, kind); + canonical = context.intern_application(canonical, rigid); + } + + let superclasses = superclasses + .into_iter() + .map(|superclass| zonk::zonk(state, context, superclass)) + .collect::>>()?; + + for (member_id, member_type) in members.iter() { + let member_type = zonk::zonk(state, context, *member_type)?; + + let toolkit::InspectQuantified { binders: member_binders, quantified: member_inner } = + toolkit::inspect_quantified(state, context, member_type)?; + + let mut result = context.intern_constrained(canonical, member_inner); + + for member_binder in member_binders.iter().cloned().rev() { + let binder_id = context.intern_forall_binder(member_binder); + result = context.intern_forall(binder_id, result); + } + + for type_parameter in type_parameters.iter().rev() { + result = context.intern_forall(*type_parameter, result); + } + + for kind_binder in kind_binders.iter().rev() { + result = context.intern_forall(*kind_binder, result); + } + + state.checked.terms.insert(*member_id, result); + } + + let members = members.into_iter().map(|(item_id, _)| item_id).collect_vec(); + + state.checked.classes.insert( + item_id, + CheckedClass { + kind_binders, + type_parameters, + canonical, + superclasses, + functional_dependencies, + members, + }, + ); + } + + Ok(()) +} From db758b258191c66bbcb18a2d13555ed461142b5c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:50:11 +0800 Subject: [PATCH 246/386] Add Classes section to checking2 test reports --- tests-integration/src/generated/basic.rs | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 9e6a74f5..505d5a7b 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -490,6 +490,42 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { writeln!(snapshot, "type {name}{binders_formatted} = {replacement}").unwrap(); } + if !checked.classes.is_empty() { + writeln!(snapshot, "\nClasses").unwrap(); + } + for (id, TypeItem { .. }) in indexed.items.iter_types() { + let Some(class) = checked.lookup_class(id) else { continue }; + + let canonical = pretty2::Pretty::new(engine).render(class.canonical); + + let mut superclasses = String::new(); + if !class.superclasses.is_empty() { + let formatted = class + .superclasses + .iter() + .map(|&superclass| pretty2::Pretty::new(engine).render(superclass)) + .collect_vec(); + superclasses = format!(" <= {}", formatted.join(", ")); + } + + let members = class + .members + .iter() + .filter_map(|&mid| { + let member_name = indexed.items[mid].name.as_deref()?; + let member_type = checked.lookup_term(mid)?; + let signature = + pretty2::Pretty::new(engine).signature(member_name).render(member_type); + Some(format!(" {signature}")) + }) + .collect_vec(); + + writeln!(snapshot, "class {canonical}{superclasses}").unwrap(); + for member in &members { + writeln!(snapshot, "{member}").unwrap(); + } + } + if !checked.roles.is_empty() { writeln!(snapshot, "\nRoles").unwrap(); } From e3a0ed2e5dd5d60c11d0083f790c349230e64bb5 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 15:51:04 +0800 Subject: [PATCH 247/386] Add integration tests for class checking --- .../checking2/009_class_check/Main.purs | 5 +++++ .../checking2/009_class_check/Main.snap | 14 +++++++++++++ .../checking2/010_class_infer/Main.purs | 4 ++++ .../checking2/010_class_infer/Main.snap | 14 +++++++++++++ .../checking2/011_class_superclass/Main.purs | 7 +++++++ .../checking2/011_class_superclass/Main.snap | 18 +++++++++++++++++ .../012_class_polykind_check/Main.purs | 5 +++++ .../012_class_polykind_check/Main.snap | 20 +++++++++++++++++++ .../013_class_polykind_infer/Main.purs | 4 ++++ .../013_class_polykind_infer/Main.snap | 20 +++++++++++++++++++ .../tests/checking2/generated.rs | 10 ++++++++++ 11 files changed, 121 insertions(+) create mode 100644 tests-integration/fixtures/checking2/009_class_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/009_class_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/010_class_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/010_class_infer/Main.snap create mode 100644 tests-integration/fixtures/checking2/011_class_superclass/Main.purs create mode 100644 tests-integration/fixtures/checking2/011_class_superclass/Main.snap create mode 100644 tests-integration/fixtures/checking2/012_class_polykind_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/013_class_polykind_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap diff --git a/tests-integration/fixtures/checking2/009_class_check/Main.purs b/tests-integration/fixtures/checking2/009_class_check/Main.purs new file mode 100644 index 00000000..bcb40a07 --- /dev/null +++ b/tests-integration/fixtures/checking2/009_class_check/Main.purs @@ -0,0 +1,5 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a where + eq :: a -> a -> Boolean diff --git a/tests-integration/fixtures/checking2/009_class_check/Main.snap b/tests-integration/fixtures/checking2/009_class_check/Main.snap new file mode 100644 index 00000000..db102bf8 --- /dev/null +++ b/tests-integration/fixtures/checking2/009_class_check/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class Eq (t0 :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/010_class_infer/Main.purs b/tests-integration/fixtures/checking2/010_class_infer/Main.purs new file mode 100644 index 00000000..40d61855 --- /dev/null +++ b/tests-integration/fixtures/checking2/010_class_infer/Main.purs @@ -0,0 +1,4 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean diff --git a/tests-integration/fixtures/checking2/010_class_infer/Main.snap b/tests-integration/fixtures/checking2/010_class_infer/Main.snap new file mode 100644 index 00000000..db102bf8 --- /dev/null +++ b/tests-integration/fixtures/checking2/010_class_infer/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class Eq (t0 :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/011_class_superclass/Main.purs b/tests-integration/fixtures/checking2/011_class_superclass/Main.purs new file mode 100644 index 00000000..927f73f5 --- /dev/null +++ b/tests-integration/fixtures/checking2/011_class_superclass/Main.purs @@ -0,0 +1,7 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean + +class Eq a <= Ord a where + compare :: a -> a -> Int diff --git a/tests-integration/fixtures/checking2/011_class_superclass/Main.snap b/tests-integration/fixtures/checking2/011_class_superclass/Main.snap new file mode 100644 index 00000000..bac92513 --- /dev/null +++ b/tests-integration/fixtures/checking2/011_class_superclass/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int + +Types +Eq :: Type -> Constraint +Ord :: Type -> Constraint + +Classes +class Eq (t0 :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +class Ord (t1 :: Type) <= Eq (t1 :: Type) + compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int diff --git a/tests-integration/fixtures/checking2/012_class_polykind_check/Main.purs b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.purs new file mode 100644 index 00000000..7ceb2d4c --- /dev/null +++ b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.purs @@ -0,0 +1,5 @@ +module Main where + +class HasKind :: forall k. k -> Constraint +class HasKind a where + reflectKind :: forall (p :: k -> Type). p a -> String diff --git a/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap new file mode 100644 index 00000000..d1faf52d --- /dev/null +++ b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +reflectKind :: + forall (k :: Type) (a :: (k :: Type)) (p :: (k :: Type) -> Type). + HasKind @(k :: Type) (a :: (k :: Type)) => + (p :: (k :: Type) -> Type) (a :: (k :: Type)) -> String + +Types +HasKind :: forall (k :: Type). (k :: Type) -> Constraint + +Classes +class HasKind @(t0 :: Type) (t1 :: (t0 :: Type)) + reflectKind :: + forall (k :: Type) (a :: (k :: Type)) (p :: (k :: Type) -> Type). + HasKind @(k :: Type) (a :: (k :: Type)) => + (p :: (k :: Type) -> Type) (a :: (k :: Type)) -> String diff --git a/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.purs b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.purs new file mode 100644 index 00000000..ebb1548f --- /dev/null +++ b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.purs @@ -0,0 +1,4 @@ +module Main where + +class HasKind' a where + reflectKind' :: forall p. p a -> String diff --git a/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap new file mode 100644 index 00000000..5ee12b39 --- /dev/null +++ b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +reflectKind' :: + forall (t2 :: Type) (a :: (t2 :: Type)) (p :: (t2 :: Type) -> Type). + HasKind' @(t2 :: Type) (a :: (t2 :: Type)) => + (p :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> String + +Types +HasKind' :: forall (t2 :: Type). (t2 :: Type) -> Constraint + +Classes +class HasKind' @(t2 :: Type) (t0 :: (t2 :: Type)) + reflectKind' :: + forall (t2 :: Type) (a :: (t2 :: Type)) (p :: (t2 :: Type) -> Type). + HasKind' @(t2 :: Type) (a :: (t2 :: Type)) => + (p :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> String diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 25dfdc7c..fdb6ce88 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -45,3 +45,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_007_synonym_check_main() { run_test("007_synonym_check", "Main"); } #[rustfmt::skip] #[test] fn test_008_synonym_infer_main() { run_test("008_synonym_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_009_class_check_main() { run_test("009_class_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_010_class_infer_main() { run_test("010_class_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_011_class_superclass_main() { run_test("011_class_superclass", "Main"); } + +#[rustfmt::skip] #[test] fn test_012_class_polykind_check_main() { run_test("012_class_polykind_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_013_class_polykind_infer_main() { run_test("013_class_polykind_infer", "Main"); } From 297e69187f8f7bc79e1448d1615ddbb6c85ac10b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 17:14:22 +0800 Subject: [PATCH 248/386] Implement operator declaration checking --- compiler-core/checking2/src/source.rs | 84 ++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 0ce861fd..3f8e33c1 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -9,6 +9,7 @@ use std::mem; use std::sync::Arc; use building_types::QueryResult; +use files::FileId; use indexing::{TermItemId, TypeItemId}; use itertools::Itertools; use lowering::{ @@ -23,7 +24,7 @@ use crate::core::{ CheckedClass, CheckedSynonym, ForallBinder, Type, TypeId, generalise, toolkit, unification, zonk, }; -use crate::error::ErrorCrumb; +use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; struct PendingDataType { @@ -48,6 +49,7 @@ struct TypeSccState { data: Vec<(TypeItemId, PendingDataType)>, synonym: Vec<(TypeItemId, PendingSynonymType)>, class: Vec<(TypeItemId, PendingClassType)>, + operator: Vec, } /// Checks all type items in topological order. @@ -86,6 +88,7 @@ where finalise_data_constructors(state, context, &mut scc_state)?; finalise_synonym_replacements(state, context, &mut scc_state)?; finalise_classes(state, context, &mut scc_state)?; + finalise_operators(state, context, &mut scc_state)?; } Ok(()) } @@ -202,7 +205,7 @@ where let Some(signature) = signature else { return Ok(()) }; check_signature_type(state, context, item_id, *signature)?; } - TypeItemIr::Operator { .. } => todo!(), + TypeItemIr::Operator { .. } => {} } Ok(()) @@ -253,7 +256,9 @@ where check_class_equation(state, context, scc, item_id, *signature, class)?; } TypeItemIr::Foreign { .. } => {} - TypeItemIr::Operator { .. } => todo!(), + TypeItemIr::Operator { resolution, .. } => { + check_operator_equation(state, context, scc, item_id, *resolution)?; + } } Ok(()) @@ -787,3 +792,76 @@ where Ok(()) } + +fn check_operator_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + resolution: Option<(FileId, TypeItemId)>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some((file_id, type_id)) = resolution else { + return Ok(()); + }; + + let kind = if file_id == context.id { + state.checked.lookup_type(type_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_type(type_id) + }; + + let kind = if let Some(kind) = kind { kind } else { context.unknown("invalid item kind") }; + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, kind, expected)?; + } else { + state.checked.types.insert(item_id, kind); + } + + scc.operator.push(item_id); + + Ok(()) +} + +fn finalise_operators( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for item_id in mem::take(&mut scc.operator) { + let Some(kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + if !is_binary_operator_type(state, context, kind)? { + let kind_message = state.pretty_id(context, kind)?; + state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); + } + } + + Ok(()) +} + +fn is_binary_operator_type( + state: &mut CheckState, + context: &CheckContext, + kind: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, kind)?; + + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, quantified)?; + + Ok(arguments.len() == 2) +} From 5d35ce3adb2a12d3445457789d5368a8b5730dc6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 17:14:27 +0800 Subject: [PATCH 249/386] Add integration tests for operator declarations --- .../014_operator_alias_kind/Main.purs | 5 +++++ .../014_operator_alias_kind/Main.snap | 13 ++++++++++++ .../015_operator_alias_invalid_kind/Main.purs | 5 +++++ .../015_operator_alias_invalid_kind/Main.snap | 21 +++++++++++++++++++ .../tests/checking2/generated.rs | 4 ++++ 5 files changed, 48 insertions(+) create mode 100644 tests-integration/fixtures/checking2/014_operator_alias_kind/Main.purs create mode 100644 tests-integration/fixtures/checking2/014_operator_alias_kind/Main.snap create mode 100644 tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.purs create mode 100644 tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap diff --git a/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.purs b/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.purs new file mode 100644 index 00000000..e1704fcd --- /dev/null +++ b/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.purs @@ -0,0 +1,5 @@ +module Main where + +type Add a b = a + +infix 5 type Add as + diff --git a/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.snap b/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.snap new file mode 100644 index 00000000..d4ca4cfd --- /dev/null +++ b/tests-integration/fixtures/checking2/014_operator_alias_kind/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) ++ :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) + +Synonyms +type Add a b = (a :: (t3 :: Type)) diff --git a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.purs b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.purs new file mode 100644 index 00000000..f4ee3580 --- /dev/null +++ b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.purs @@ -0,0 +1,5 @@ +module Main where + +type Identity a = a + +infix 5 type Identity as + diff --git a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap new file mode 100644 index 00000000..aa9bc302 --- /dev/null +++ b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Identity :: forall (t1 :: Type). (t1 :: Type) -> (t1 :: Type) ++ :: forall (t1 :: Type). (t1 :: Type) -> (t1 :: Type) + +Synonyms +type Identity a = (a :: (t1 :: Type)) + +Errors +CheckError { + kind: InvalidTypeOperator { + kind_message: Id(2), + }, + crumbs: [], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index fdb6ce88..706ae4ba 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -55,3 +55,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_012_class_polykind_check_main() { run_test("012_class_polykind_check", "Main"); } #[rustfmt::skip] #[test] fn test_013_class_polykind_infer_main() { run_test("013_class_polykind_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_014_operator_alias_kind_main() { run_test("014_operator_alias_kind", "Main"); } + +#[rustfmt::skip] #[test] fn test_015_operator_alias_invalid_kind_main() { run_test("015_operator_alias_invalid_kind", "Main"); } From 4aeba3b245566591c20e87e681186dbb7bea18f6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 20:28:08 +0800 Subject: [PATCH 250/386] Move lookup functions to toolkit --- compiler-compatibility/command/src/compat.rs | 8 +- compiler-core/checking2/src/core.rs | 3 +- compiler-core/checking2/src/core/toolkit.rs | 78 ++++++++++++++++++- compiler-core/checking2/src/source.rs | 39 ++++------ compiler-core/checking2/src/source/synonym.rs | 25 +----- compiler-core/checking2/src/source/types.rs | 31 ++------ tests-integration/src/generated/basic.rs | 2 +- 7 files changed, 111 insertions(+), 75 deletions(-) diff --git a/compiler-compatibility/command/src/compat.rs b/compiler-compatibility/command/src/compat.rs index 0d231c50..f411b6c8 100644 --- a/compiler-compatibility/command/src/compat.rs +++ b/compiler-compatibility/command/src/compat.rs @@ -415,9 +415,11 @@ fn find_package_dir(packages_dir: &Path, package_name: &str) -> Option().is_ok() && entry.file_type().ok()?.is_dir() { - return entry.path().canonicalize().ok(); - } + && suffix.parse::().is_ok() + && entry.file_type().ok()?.is_dir() + { + return entry.path().canonicalize().ok(); + } } None } diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 6260d666..f50c8b49 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -105,8 +105,9 @@ pub struct Synonym { #[derive(Debug, Clone, PartialEq, Eq)] pub struct CheckedSynonym { + pub kind: TypeId, pub parameters: Vec, - pub replacement: TypeId, + pub synonym: TypeId, } /// Represents a checked class declaration. diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index c29c4897..174bcc6e 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -1,9 +1,11 @@ //! Implements shared utilities for core type operations. use building_types::QueryResult; +use files::FileId; +use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; -use crate::core::{ForallBinder, Type, TypeId, normalise}; +use crate::core::{CheckedSynonym, ForallBinder, Type, TypeId, normalise}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -17,6 +19,80 @@ pub struct InspectFunction { pub result: TypeId, } +pub fn lookup_file_type( + state: &CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let kind = if file_id == context.id { + state.checked.lookup_type(type_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_type(type_id) + }; + + if let Some(kind) = kind { Ok(kind) } else { Ok(context.unknown("invalid type item")) } +} + +pub fn lookup_file_term( + state: &CheckState, + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let term = if file_id == context.id { + state.checked.lookup_term(term_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_term(term_id) + }; + + if let Some(term) = term { Ok(term) } else { Ok(context.unknown("invalid term item")) } +} + +pub fn lookup_file_synonym( + state: &CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + if file_id == context.id { + Ok(state.checked.lookup_synonym(type_id)) + } else { + let checked = context.queries.checked2(file_id)?; + Ok(checked.lookup_synonym(type_id)) + } +} + +pub fn lookup_file_operator( + state: &CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let kind = if file_id == context.id { + state.checked.lookup_type(type_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_type(type_id) + }; + + if let Some(kind) = kind { Ok(kind) } else { Ok(context.unknown("invalid operator item")) } +} + pub fn inspect_quantified( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 3f8e33c1..1f554b35 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -33,8 +33,9 @@ struct PendingDataType { } struct PendingSynonymType { + kind: TypeId, parameters: Vec, - replacement: TypeId, + synonym: TypeId, } struct PendingClassType { @@ -513,7 +514,7 @@ fn check_synonym_equation( where Q: ExternalQueries, { - let (parameters, result) = if let Some(signature_id) = signature + let (parameters, kind, result) = if let Some(signature_id) = signature && let Some(signature_kind) = state.checked.lookup_type(item_id) { check_synonym_equation_check(state, context, bindings, (signature_id, signature_kind))? @@ -521,14 +522,14 @@ where check_synonym_equation_infer(state, context, item_id, bindings)? }; - let replacement = if let Some(synonym) = synonym { + let synonym = if let Some(synonym) = synonym { let (synonym, _) = types::check_kind(state, context, synonym, result)?; synonym } else { context.unknown("invalid synonym type") }; - scc.synonym.push((item_id, PendingSynonymType { parameters, replacement })); + scc.synonym.push((item_id, PendingSynonymType { kind, parameters, synonym })); Ok(()) } @@ -537,14 +538,15 @@ fn check_synonym_equation_check( state: &mut CheckState, context: &CheckContext, bindings: &[TypeVariableBinding], - signature: (lowering::TypeId, TypeId), -) -> QueryResult<(Vec, TypeId)> + (signature_id, signature_kind): (lowering::TypeId, TypeId), +) -> QueryResult<(Vec, TypeId, TypeId)> where Q: ExternalQueries, { - let signature = signature::inspect_signature(state, context, signature, bindings)?; + let signature = + signature::inspect_signature(state, context, (signature_id, signature_kind), bindings)?; let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; - Ok((parameters, signature.result)) + Ok((parameters, signature_kind, signature.result)) } fn check_synonym_equation_infer( @@ -552,7 +554,7 @@ fn check_synonym_equation_infer( context: &CheckContext, item_id: TypeItemId, bindings: &[TypeVariableBinding], -) -> QueryResult<(Vec, TypeId)> +) -> QueryResult<(Vec, TypeId, TypeId)> where Q: ExternalQueries, { @@ -567,7 +569,7 @@ where state.checked.types.insert(item_id, inferred); } - Ok((bindings, result)) + Ok((bindings, inferred, result)) } fn finalise_synonym_replacements( @@ -578,10 +580,10 @@ fn finalise_synonym_replacements( where Q: ExternalQueries, { - for (item_id, PendingSynonymType { parameters, replacement }) in mem::take(&mut scc.synonym) { - let replacement = zonk::zonk(state, context, replacement)?; - let checked_synonym = CheckedSynonym { parameters, replacement }; - state.checked.synonyms.insert(item_id, checked_synonym); + for (item_id, PendingSynonymType { kind, parameters, synonym }) in mem::take(&mut scc.synonym) { + let synonym = zonk::zonk(state, context, synonym)?; + let synonym = CheckedSynonym { kind, parameters, synonym }; + state.checked.synonyms.insert(item_id, synonym); } Ok(()) } @@ -807,14 +809,7 @@ where return Ok(()); }; - let kind = if file_id == context.id { - state.checked.lookup_type(type_id) - } else { - let checked = context.queries.checked2(file_id)?; - checked.lookup_type(type_id) - }; - - let kind = if let Some(kind) = kind { kind } else { context.unknown("invalid item kind") }; + let kind = toolkit::lookup_file_operator(state, context, file_id, type_id)?; if let Some(expected) = state.checked.lookup_type(item_id) { unification::subtype(state, context, kind, expected)?; diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index e86e4e9f..b8402e40 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -9,30 +9,11 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{CheckedSynonym, Saturation, Synonym, Type, TypeId, normalise, unification}; +use crate::core::{Saturation, Synonym, Type, TypeId, normalise, toolkit, unification}; use crate::error::ErrorKind; use crate::source::types; use crate::state::CheckState; -pub fn lookup_file_synonym( - state: &mut CheckState, - context: &CheckContext, - file_id: FileId, - type_id: TypeItemId, -) -> QueryResult> -where - Q: ExternalQueries, -{ - let (synonym, kind) = if file_id == context.id { - (state.checked.lookup_synonym(type_id), state.checked.lookup_type(type_id)) - } else { - let checked = context.queries.checked2(file_id)?; - (checked.lookup_synonym(type_id), checked.lookup_type(type_id)) - }; - - Ok(synonym.zip(kind)) -} - pub fn parse_synonym_application( state: &mut CheckState, context: &CheckContext, @@ -51,12 +32,14 @@ where return Ok(None); }; - let Some((checked_synonym, kind)) = lookup_file_synonym(state, context, file_id, type_id)? + let Some(checked_synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? else { return Ok(None); }; + let kind = checked_synonym.kind; let arity = checked_synonym.parameters.len(); + Ok(Some((file_id, type_id, kind, arity))) } diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 2742c196..46434ffb 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -3,13 +3,11 @@ use std::sync::Arc; use building_types::QueryResult; -use files::FileId; -use indexing::TypeItemId; use smol_str::SmolStr; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise, unification}; +use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise, toolkit, unification}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::synonym; use crate::state::CheckState; @@ -148,14 +146,13 @@ where return Ok(unknown("missing constructor")); }; - if let Some((synonym, kind)) = - synonym::lookup_file_synonym(state, context, file_id, type_id)? + if let Some(synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? { - let synonym = (file_id, type_id, kind, synonym.parameters.len()); + let synonym = (file_id, type_id, synonym.kind, synonym.parameters.len()); return synonym::infer_synonym_constructor(state, context, synonym, id); } let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let k = lookup_file_type(state, context, file_id, type_id)?; + let k = toolkit::lookup_file_type(state, context, file_id, type_id)?; Ok((t, k)) } @@ -220,7 +217,7 @@ where }; let t = context.queries.intern_type(Type::OperatorConstructor(file_id, type_id)); - let k = lookup_file_type(state, context, file_id, type_id)?; + let k = toolkit::lookup_file_type(state, context, file_id, type_id)?; Ok((t, k)) } @@ -349,24 +346,6 @@ where Ok((t, k)) } -fn lookup_file_type( - state: &mut CheckState, - context: &CheckContext, - file_id: FileId, - type_id: TypeItemId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let result = if file_id == context.id { - state.checked.types.get(&type_id).copied() - } else { - let checked = context.queries.checked2(file_id)?; - checked.types.get(&type_id).copied() - }; - Ok(result.unwrap_or_else(|| context.unknown("kind"))) -} - fn check_type_variable_binding( state: &mut CheckState, context: &CheckContext, diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 505d5a7b..102fc69c 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -483,7 +483,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { let Some(name) = name else { continue }; let Some(definition) = checked.lookup_synonym(id) else { continue }; let names = definition.parameters.iter().map(|b| (b.name, b.text.clone())); - let replacement = pretty2::Pretty::new(engine).names(names).render(definition.replacement); + let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); let binders = definition.parameters.iter().map(|b| b.text.as_str()).collect_vec(); let binders_formatted = if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; From 11321e9ff439fb3d431e8c0bd9adf12fa20d5438 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 24 Feb 2026 21:48:43 +0800 Subject: [PATCH 251/386] Name::as_text for missing text --- compiler-core/checking2/src/source.rs | 4 +--- compiler-core/checking2/src/source/types.rs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 1f554b35..4b87ab41 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -16,7 +16,6 @@ use lowering::{ ClassIr, DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, TypeItemIr, TypeVariableBinding, }; -use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; @@ -322,8 +321,7 @@ where let name = state.names.fresh(); state.kind_scope.bind_forall(equation_binding.id, name, kind); - const MISSING: SmolStr = SmolStr::new_static(""); - let text = equation_binding.name.clone().unwrap_or(MISSING); + let text = equation_binding.name.clone().unwrap_or_else(|| name.as_text()); let visible = equation_binding.visible; binders.push(ForallBinder { visible, name, text, kind }); diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 46434ffb..1d4243a3 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -146,8 +146,7 @@ where return Ok(unknown("missing constructor")); }; - if let Some(synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? - { + if let Some(synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? { let synonym = (file_id, type_id, synonym.kind, synonym.parameters.len()); return synonym::infer_synonym_constructor(state, context, synonym, id); } @@ -354,8 +353,6 @@ fn check_type_variable_binding( where Q: ExternalQueries, { - let text = binding.name.clone().unwrap_or(MISSING_NAME); - let kind = if let Some(kind_id) = binding.kind { let (kind, _) = check_kind(state, context, kind_id, context.prim.t)?; kind @@ -363,10 +360,12 @@ where state.fresh_unification(context.queries, context.prim.t) }; + let visible = binding.visible; let name = state.names.fresh(); - state.kind_scope.bind_forall(binding.id, name, kind); + let text = if let Some(name) = &binding.name { SmolStr::clone(name) } else { name.as_text() }; - Ok(ForallBinder { visible: binding.visible, name, text, kind }) + state.kind_scope.bind_forall(binding.id, name, kind); + Ok(ForallBinder { visible, name, text, kind }) } pub fn infer_application_kind( From 47a817eefe219eca89943ac0ad1f91ef98f9af33 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 00:50:56 +0800 Subject: [PATCH 252/386] Implement operator chain checking for types --- compiler-core/checking2/src/source.rs | 1 + .../checking2/src/source/operator.rs | 316 ++++++++++++++++++ compiler-core/checking2/src/source/types.rs | 4 +- 3 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 compiler-core/checking2/src/source/operator.rs diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 4b87ab41..8e9e4884 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -1,5 +1,6 @@ //! Implements syntax-driven checking rules for source files. +pub mod operator; pub mod signature; pub mod synonym; pub mod terms; diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs new file mode 100644 index 00000000..1715bc28 --- /dev/null +++ b/compiler-core/checking2/src/source/operator.rs @@ -0,0 +1,316 @@ +//! Implements surface-generic operator chain inference. + +use building_types::QueryResult; +use files::FileId; +use lowering::IsElement; +use sugar::OperatorTree; +use sugar::bracketing::BracketingResult; + +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, toolkit, unification}; +use crate::source::types; +use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; + +#[derive(Copy, Clone, Debug)] +enum OperatorKindMode { + Infer, + Check { expected_type: TypeId }, +} + +pub fn infer_operator_chain( + state: &mut CheckState, + context: &CheckContext, + id: E, +) -> QueryResult<(E::Elaborated, TypeId)> +where + Q: ExternalQueries, + E: IsOperator, +{ + let unknown = (E::unknown_elaborated(context), context.unknown("invalid operator chain")); + + let Some(operator_tree) = E::lookup_tree(context, id) else { + return Ok(unknown); + }; + + let Ok(operator_tree) = operator_tree else { + return Ok(unknown); + }; + + traverse_operator_tree(state, context, operator_tree, OperatorKindMode::Infer) +} + +fn traverse_operator_tree( + state: &mut CheckState, + context: &CheckContext, + operator_tree: &OperatorTree, + mode: OperatorKindMode, +) -> QueryResult<(E::Elaborated, TypeId)> +where + Q: ExternalQueries, + E: IsOperator, +{ + let unknown_elaborated = E::unknown_elaborated(context); + + match operator_tree { + OperatorTree::Leaf(None) => match mode { + OperatorKindMode::Infer => { + Ok((unknown_elaborated, context.unknown("missing operator leaf"))) + } + OperatorKindMode::Check { expected_type } => Ok((unknown_elaborated, expected_type)), + }, + + OperatorTree::Leaf(Some(type_id)) => match mode { + OperatorKindMode::Infer => E::infer_surface(state, context, *type_id), + OperatorKindMode::Check { expected_type } => { + E::check_surface(state, context, *type_id, expected_type) + } + }, + + OperatorTree::Branch(operator_id, children) => { + let Some((file_id, item_id)) = E::lookup_operator(context, *operator_id) else { + return match mode { + OperatorKindMode::Infer => { + Ok((unknown_elaborated, context.unknown("missing operator resolution"))) + } + OperatorKindMode::Check { expected_type } => { + Ok((unknown_elaborated, expected_type)) + } + }; + }; + + let operator_type = E::lookup_item(state, context, file_id, item_id)?; + + traverse_operator_branch( + state, + context, + (file_id, item_id), + operator_type, + children, + mode, + ) + } + } +} + +fn traverse_operator_branch( + state: &mut CheckState, + context: &CheckContext, + operator: (FileId, E::ItemId), + operator_type: TypeId, + children: &[OperatorTree; 2], + mode: OperatorKindMode, +) -> QueryResult<(E::Elaborated, TypeId)> +where + Q: ExternalQueries, + E: IsOperator, +{ + let unknown_elaborated = E::unknown_elaborated(context); + let unknown = match mode { + OperatorKindMode::Infer => (unknown_elaborated, context.unknown("invalid operator kind")), + OperatorKindMode::Check { expected_type } => (unknown_elaborated, expected_type), + }; + + let operator_type = instantiate_foralls(state, context, operator_type)?; + + let Some((left_type, operator_type)) = decompose_function_kind(state, context, operator_type)? + else { + return Ok(unknown); + }; + + let Some((right_type, result_type)) = decompose_function_kind(state, context, operator_type)? + else { + return Ok(unknown); + }; + + if let OperatorKindMode::Check { expected_type } = mode { + let _ = unification::subtype(state, context, result_type, expected_type)?; + } + + let [left_tree, right_tree] = children; + + let (left, _) = traverse_operator_tree( + state, + context, + left_tree, + OperatorKindMode::Check { expected_type: left_type }, + )?; + + let (right, _) = traverse_operator_tree( + state, + context, + right_tree, + OperatorKindMode::Check { expected_type: right_type }, + )?; + + E::build(state, context, operator, (left, right), result_type) +} + +fn instantiate_foralls( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + + let Type::Forall(binder_id, inner) = context.lookup_type(id) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + id = SubstituteName::one(state, context, binder.name, replacement, inner)?; + } + + Ok(id) +} + +fn decompose_function_kind( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + + match context.lookup_type(id) { + Type::Function(argument, result) => Ok(Some((argument, result))), + + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + + let function_u = context.intern_function(argument_u, result_u); + let _ = unification::solve(state, context, id, unification_id, function_u)?; + + Ok(Some((argument_u, result_u))) + } + + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + let inner = SubstituteName::one(state, context, binder.name, replacement, inner)?; + + decompose_function_kind(state, context, inner) + } + + _ => Ok(None), + } +} + +pub trait IsOperator: IsElement { + type ItemId: Copy; + type Elaborated: Copy; + + fn unknown_elaborated(context: &CheckContext) -> Self::Elaborated; + + fn lookup_tree<'q>( + context: &'q CheckContext, + id: Self, + ) -> Option<&'q BracketingResult>; + + fn lookup_operator( + context: &CheckContext, + id: Self::OperatorId, + ) -> Option<(FileId, Self::ItemId)>; + + fn lookup_item( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: Self::ItemId, + ) -> QueryResult; + + fn infer_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + ) -> QueryResult<(Self::Elaborated, TypeId)>; + + fn check_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + expected: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)>; + + fn build( + state: &mut CheckState, + context: &CheckContext, + operator: (FileId, Self::ItemId), + result_tree: (Self::Elaborated, Self::Elaborated), + result_type: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)>; +} + +impl IsOperator for lowering::TypeId { + type ItemId = indexing::TypeItemId; + type Elaborated = TypeId; + + fn unknown_elaborated(context: &CheckContext) -> Self::Elaborated { + context.unknown("invalid operator chain") + } + + fn lookup_tree<'q>( + context: &'q CheckContext, + id: Self, + ) -> Option<&'q BracketingResult> { + context.bracketed.types.get(&id) + } + + fn lookup_operator( + context: &CheckContext, + id: Self::OperatorId, + ) -> Option<(FileId, Self::ItemId)> { + context.lowered.info.get_type_operator(id) + } + + fn lookup_item( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: Self::ItemId, + ) -> QueryResult { + toolkit::lookup_file_operator(state, context, file_id, item_id) + } + + fn infer_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + types::infer_kind(state, context, id) + } + + fn check_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + expected: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + types::check_kind(state, context, id, expected) + } + + fn build( + state: &mut CheckState, + context: &CheckContext, + (file_id, item_id): (FileId, Self::ItemId), + (left, right): (Self::Elaborated, Self::Elaborated), + result_kind: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let elaborated_type = context.intern_operator_application(file_id, item_id, left, right); + let result_kind = normalise::normalise(state, context, result_kind)?; + Ok((elaborated_type, result_kind)) + } +} diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 1d4243a3..84ebd1a8 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -9,7 +9,7 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise, toolkit, unification}; use crate::error::{ErrorCrumb, ErrorKind}; -use crate::source::synonym; +use crate::source::{operator, synonym}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -222,7 +222,7 @@ where } lowering::TypeKind::OperatorChain { .. } => { - todo!("operator chain inference") + operator::infer_operator_chain(state, context, id) } lowering::TypeKind::String { kind, value } => { From 1c8a60fd32007618e29b7ddbd036cab3d939684b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 00:50:56 +0800 Subject: [PATCH 253/386] Implement integration tests for operator chains --- .../016_type_operator_chain_infer/Main.purs | 9 +++++ .../016_type_operator_chain_infer/Main.snap | 21 ++++++++++++ .../017_type_operator_chain_check/Main.purs | 12 +++++++ .../017_type_operator_chain_check/Main.snap | 17 ++++++++++ .../Main.purs | 12 +++++++ .../Main.snap | 17 ++++++++++ .../Main.purs | 13 +++++++ .../Main.snap | 24 +++++++++++++ .../Main.purs | 8 +++++ .../Main.snap | 34 +++++++++++++++++++ .../tests/checking2/generated.rs | 10 ++++++ 11 files changed, 177 insertions(+) create mode 100644 tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap create mode 100644 tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.purs create mode 100644 tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap create mode 100644 tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.purs create mode 100644 tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap create mode 100644 tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap diff --git a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.purs b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.purs new file mode 100644 index 00000000..6adb93d7 --- /dev/null +++ b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.purs @@ -0,0 +1,9 @@ +module Main where + +type Add a b = a + +infixl 5 type Add as + + +type Chain a b c = a + b + c + +type ChainParen a b c = (a + b) + c diff --git a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap new file mode 100644 index 00000000..25dd4865 --- /dev/null +++ b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) ++ :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) +Chain :: + forall (t9 :: Type) (t8 :: Type) (t7 :: Type). + (t9 :: Type) -> (t8 :: Type) -> (t7 :: Type) -> (t9 :: Type) +ChainParen :: + forall (t15 :: Type) (t14 :: Type) (t13 :: Type). + (t15 :: Type) -> (t14 :: Type) -> (t13 :: Type) -> (t15 :: Type) + +Synonyms +type Add a b = (a :: (t3 :: Type)) +type Chain a b c = (a :: (t9 :: Type)) + (b :: (t8 :: Type)) + (c :: (t7 :: Type)) +type ChainParen a b c = (a :: (t15 :: Type)) + (b :: (t14 :: Type)) + (c :: (t13 :: Type)) diff --git a/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.purs b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.purs new file mode 100644 index 00000000..5adc98c5 --- /dev/null +++ b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.purs @@ -0,0 +1,12 @@ +module Main where + +type Add :: Type -> Type -> Type +type Add a b = a + +infixl 5 type Add as + + +type Chain :: Type -> Type -> Type -> Type +type Chain a b c = a + b + c + +type ChainParen :: Type -> Type -> Type -> Type +type ChainParen a b c = (a + b) + c diff --git a/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap new file mode 100644 index 00000000..55779add --- /dev/null +++ b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap @@ -0,0 +1,17 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: Type -> Type -> Type ++ :: Type -> Type -> Type +Chain :: Type -> Type -> Type -> Type +ChainParen :: Type -> Type -> Type -> Type + +Synonyms +type Add a b = (a :: Type) +type Chain a b c = (a :: Type) + (b :: Type) + (c :: Type) +type ChainParen a b c = (a :: Type) + (b :: Type) + (c :: Type) diff --git a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.purs b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.purs new file mode 100644 index 00000000..61985017 --- /dev/null +++ b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.purs @@ -0,0 +1,12 @@ +module Main where + +type Add :: forall k. k -> k -> k +type Add a b = a + +infixl 5 type Add as + + +type Chain :: forall k. k -> k -> k -> k +type Chain a b c = a + b + c + +type ChainParen :: forall k. k -> k -> k -> k +type ChainParen a b c = (a + b) + c diff --git a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap new file mode 100644 index 00000000..e85beffc --- /dev/null +++ b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap @@ -0,0 +1,17 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) ++ :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) +Chain :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) -> (k :: Type) +ChainParen :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) -> (k :: Type) + +Synonyms +type Add a b = (a :: (t0 :: Type)) +type Chain a b c = (a :: (t3 :: Type)) + (b :: (t3 :: Type)) + (c :: (t3 :: Type)) +type ChainParen a b c = (a :: (t7 :: Type)) + (b :: (t7 :: Type)) + (c :: (t7 :: Type)) diff --git a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.purs b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.purs new file mode 100644 index 00000000..ae742b36 --- /dev/null +++ b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.purs @@ -0,0 +1,13 @@ +module Main where + +type Add a b = a + +infixl 5 type Add as :+: + +type Mul a b = a + +infixl 6 type Mul as :*: + +type Chain a b c = a :+: b :*: c + +type ChainParen a b c = (a :+: b) :*: c diff --git a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap new file mode 100644 index 00000000..3033329d --- /dev/null +++ b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) +:+: :: forall (t3 :: Type) (t2 :: Type). (t3 :: Type) -> (t2 :: Type) -> (t3 :: Type) +Mul :: forall (t7 :: Type) (t6 :: Type). (t7 :: Type) -> (t6 :: Type) -> (t7 :: Type) +:*: :: forall (t7 :: Type) (t6 :: Type). (t7 :: Type) -> (t6 :: Type) -> (t7 :: Type) +Chain :: + forall (t13 :: Type) (t12 :: Type) (t11 :: Type). + (t13 :: Type) -> (t12 :: Type) -> (t11 :: Type) -> (t13 :: Type) +ChainParen :: + forall (t19 :: Type) (t18 :: Type) (t17 :: Type). + (t19 :: Type) -> (t18 :: Type) -> (t17 :: Type) -> (t19 :: Type) + +Synonyms +type Add a b = (a :: (t3 :: Type)) +type Mul a b = (a :: (t7 :: Type)) +type Chain a b c = (a :: (t13 :: Type)) :+: (b :: (t12 :: Type)) :*: (c :: (t11 :: Type)) +type ChainParen a b c = (a :: (t19 :: Type)) :+: (b :: (t18 :: Type)) :*: (c :: (t17 :: Type)) diff --git a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.purs b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.purs new file mode 100644 index 00000000..4e67bd1b --- /dev/null +++ b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.purs @@ -0,0 +1,8 @@ +module Main where + +type Add :: Type -> Type -> Type +type Add a b = a + +infixl 5 type Add as + + +type Bad = Int + "hello" diff --git a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap new file mode 100644 index 00000000..e9b84989 --- /dev/null +++ b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Add :: Type -> Type -> Type ++ :: Type -> Type -> Type +Bad :: Type + +Synonyms +type Add a b = (a :: Type) +type Bad = Int + "hello" + +Errors +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [ + CheckingKind( + AstId(17), + ), + InferringKind( + AstId(17), + ), + CheckingKind( + AstId(21), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 706ae4ba..f0b61cec 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -59,3 +59,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_014_operator_alias_kind_main() { run_test("014_operator_alias_kind", "Main"); } #[rustfmt::skip] #[test] fn test_015_operator_alias_invalid_kind_main() { run_test("015_operator_alias_invalid_kind", "Main"); } + +#[rustfmt::skip] #[test] fn test_016_type_operator_chain_infer_main() { run_test("016_type_operator_chain_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_017_type_operator_chain_check_main() { run_test("017_type_operator_chain_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_018_type_operator_chain_polykind_main() { run_test("018_type_operator_chain_polykind", "Main"); } + +#[rustfmt::skip] #[test] fn test_019_type_operator_chain_precedence_main() { run_test("019_type_operator_chain_precedence", "Main"); } + +#[rustfmt::skip] #[test] fn test_020_type_operator_chain_kind_error_main() { run_test("020_type_operator_chain_kind_error", "Main"); } From 2c9c7e6bfcf1f423185a942dee48c13d80d33089 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 01:23:59 +0800 Subject: [PATCH 254/386] Implement initial elaborate_kind --- .../checking2/src/core/unification.rs | 5 +- .../checking2/src/source/operator.rs | 21 ++++ compiler-core/checking2/src/source/types.rs | 119 ++++++++++++++++++ .../015_operator_alias_invalid_kind/Main.snap | 2 +- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 766fec92..03e20be3 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -11,6 +11,7 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise}; use crate::error::ErrorKind; +use crate::source::types; use crate::state::{CheckState, UnificationEntry}; /// Strategy for handling constrained types during [`subtype_with`]. @@ -392,8 +393,8 @@ where } let unification_kind = state.unifications.get(id).kind; - // TODO: unify kinds once kind elaboration is available - let _ = unification_kind; + let solution_kind = types::elaborate_kind(state, context, solution)?; + unify(state, context, unification_kind, solution_kind)?; state.unifications.solve(id, solution); Ok(true) diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 1715bc28..b89e13da 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -2,6 +2,7 @@ use building_types::QueryResult; use files::FileId; +use indexing::TypeItemId; use lowering::IsElement; use sugar::OperatorTree; use sugar::bracketing::BracketingResult; @@ -209,6 +210,26 @@ where } } +pub fn elaborate_operator_application_kind( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let operator_kind = toolkit::lookup_file_type(state, context, file_id, type_id)?; + let operator_kind = instantiate_foralls(state, context, operator_kind)?; + let operator_function = toolkit::inspect_function(state, context, operator_kind)?; + + if operator_function.arguments.len() >= 2 { + Ok(operator_function.result) + } else { + Ok(context.unknown("invalid operator kind")) + } +} + pub trait IsOperator: IsElement { type ItemId: Copy; type Elaborated: Copy; diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 84ebd1a8..13bb1e98 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -481,3 +481,122 @@ where Ok((row_type, row_kind)) } + +pub fn elaborate_kind( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let unknown = context.unknown("invalid kind"); + let id = normalise::normalise(state, context, id)?; + + let kind = match context.lookup_type(id) { + Type::Application(function, _) => { + let function_kind = elaborate_kind(state, context, function)?; + let function_kind = normalise::normalise(state, context, function_kind)?; + + match context.lookup_type(function_kind) { + Type::Function(_, result_kind) => result_kind, + + Type::Unification(unification_id) => { + let depth = state.unifications.get(unification_id).depth; + + let argument_u = state.unifications.fresh(depth, context.prim.t); + let argument_u = context.queries.intern_type(Type::Unification(argument_u)); + + let result_u = state.unifications.fresh(depth, context.prim.t); + let result_u = context.queries.intern_type(Type::Unification(result_u)); + + let function_u = context.intern_function(argument_u, result_u); + unification::solve(state, context, function_kind, unification_id, function_u)?; + + result_u + } + + _ => unknown, + } + } + + Type::KindApplication(function, argument) => { + let function_kind = elaborate_kind(state, context, function)?; + let function_kind = normalise::normalise(state, context, function_kind)?; + + match context.lookup_type(function_kind) { + Type::Forall(binder_id, inner_kind) => { + let binder = context.lookup_forall_binder(binder_id); + let argument = normalise::normalise(state, context, argument)?; + SubstituteName::one(state, context, binder.name, argument, inner_kind)? + } + _ => unknown, + } + } + + Type::Constrained(_, _) => context.prim.t, + Type::Forall(_, _) => context.prim.t, + Type::Function(_, _) => context.prim.t, + + Type::Constructor(file_id, type_id) => { + toolkit::lookup_file_type(state, context, file_id, type_id)? + } + + Type::OperatorConstructor(file_id, type_id) => { + toolkit::lookup_file_type(state, context, file_id, type_id)? + } + + Type::OperatorApplication(file_id, type_id, _, _) => { + operator::elaborate_operator_application_kind(state, context, file_id, type_id)? + } + + Type::Integer(_) => context.prim.int, + Type::String(_, _) => context.prim.symbol, + Type::Kinded(_, kind) => kind, + + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + let fields = Arc::clone(&row.fields); + + let field_kind = state.fresh_unification(context.queries, context.prim.t); + let tail_kind = context.intern_application(context.prim.row, field_kind); + + for field in fields.iter() { + let kind = elaborate_kind(state, context, field.id)?; + unification::unify(state, context, field_kind, kind)?; + } + + if let Some(tail) = row.tail { + let kind = elaborate_kind(state, context, tail)?; + unification::unify(state, context, tail_kind, kind)?; + } + + context.intern_application(context.prim.row, field_kind) + } + + Type::SynonymApplication(synonym_id) => { + let synonym = context.lookup_synonym(synonym_id); + let (file_id, type_id) = synonym.reference; + let arguments = Arc::clone(&synonym.arguments); + + let mut synonym_kind = toolkit::lookup_file_type(state, context, file_id, type_id)?; + + for _ in arguments.iter() { + synonym_kind = normalise::normalise(state, context, synonym_kind)?; + match context.lookup_type(synonym_kind) { + Type::Function(_, result_kind) => synonym_kind = result_kind, + _ => return Ok(unknown), + } + } + + synonym_kind + } + + Type::Unification(unification_id) => state.unifications.get(unification_id).kind, + Type::Rigid(_, _, kind) => kind, + Type::Free(_) => unknown, + Type::Unknown(_) => unknown, + }; + + Ok(kind) +} diff --git a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap index aa9bc302..e79e718a 100644 --- a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap +++ b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap @@ -15,7 +15,7 @@ type Identity a = (a :: (t1 :: Type)) Errors CheckError { kind: InvalidTypeOperator { - kind_message: Id(2), + kind_message: Id(3), }, crumbs: [], } From dd8fa1a4017b9eb7d2a9a9aec9bbd3df7f8b6668 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 01:24:04 +0800 Subject: [PATCH 255/386] Improve formatting code for integration tests --- compiler-core/stabilizing/src/id.rs | 12 + .../136_derive_nested_higher_kinded/Main.snap | 4 +- .../checking/167_derive_eq_1/Main.snap | 12 +- .../checking/168_derive_ord_1/Main.snap | 14 +- tests-integration/src/generated/basic.rs | 494 ++++++++---------- 5 files changed, 253 insertions(+), 283 deletions(-) diff --git a/compiler-core/stabilizing/src/id.rs b/compiler-core/stabilizing/src/id.rs index a52389dc..9e805699 100644 --- a/compiler-core/stabilizing/src/id.rs +++ b/compiler-core/stabilizing/src/id.rs @@ -38,6 +38,18 @@ impl> PartialEq for AstId { impl> Eq for AstId {} +impl> PartialOrd for AstId { + fn partial_cmp(&self, other: &AstId) -> Option { + Some(self.cmp(other)) + } +} + +impl> Ord for AstId { + fn cmp(&self, other: &AstId) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + impl> hash::Hash for AstId { fn hash(&self, state: &mut H) { self.id.hash(state); diff --git a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap index 541f8ed1..53c6ec12 100644 --- a/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking/136_derive_nested_higher_kinded/Main.snap @@ -47,14 +47,14 @@ U = [Representational] V = [Representational, Representational] Derived -derive Eq1 (f :: Type -> Type) => Eq (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) -derive Ord1 (f :: Type -> Type) => Ord (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) derive Eq1 (f :: Type -> Type) => Eq (T @Type (f :: Type -> Type) (g :: Type -> Type) :: Type) derive Ord1 (f :: Type -> Type) => Ord (T @Type (f :: Type -> Type) (g :: Type -> Type) :: Type) derive Eq (a :: Type) => Eq (Maybe (a :: Type) :: Type) derive Ord (a :: Type) => Ord (Maybe (a :: Type) :: Type) derive Eq1 (f :: Type -> Type) => Eq (U (f :: Type -> Type) :: Type) derive Ord1 (f :: Type -> Type) => Ord (U (f :: Type -> Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) +derive Ord1 (f :: Type -> Type) => Ord (V @Type (f :: Type -> Type) (g :: Int -> Type) :: Type) Diagnostics error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) Int) diff --git a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap index 6f001319..df23fb5e 100644 --- a/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking/167_derive_eq_1/Main.snap @@ -81,13 +81,7 @@ Either' = [Representational] NoEq = [Representational] Derived -derive Eq1 (f :: Type -> Type) => Eq1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) -derive forall (t41 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)))) => Eq (Compose @Type @(t41 :: Type) (f :: Type -> Type) (g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)) :: Type) -derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) derive Eq (a :: Type) => Eq (Id (a :: Type) :: Type) -derive Eq (a :: Type) => Eq (Either' (a :: Type) :: Type) -derive Eq1 (Either' :: Type -> Type) -derive Eq1 (NoEq :: Type -> Type) derive Eq1 (Id :: Type -> Type) derive Eq (a :: Type) => Eq (Pair (a :: Type) :: Type) derive Eq1 (Pair :: Type -> Type) @@ -96,6 +90,12 @@ derive Eq1 (Mixed :: Type -> Type) derive Eq (a :: Type) => Eq (Rec (a :: Type) :: Type) derive Eq1 (Rec :: Type -> Type) derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive forall (t41 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)))) => Eq (Compose @Type @(t41 :: Type) (f :: Type -> Type) (g :: (t41 :: Type) -> Type) (a :: (t41 :: Type)) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Eq (a :: Type) => Eq (Either' (a :: Type) :: Type) +derive Eq1 (Either' :: Type -> Type) +derive Eq1 (NoEq :: Type -> Type) Diagnostics error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) (~_ :: Type)) diff --git a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap index 080b13c9..dd3484b5 100644 --- a/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking/168_derive_ord_1/Main.snap @@ -52,13 +52,6 @@ Compose = [Representational, Representational, Nominal] NoOrd = [Representational] Derived -derive forall (t40 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)))) => Eq (Compose @Type @(t40 :: Type) (f :: Type -> Type) (g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)) :: Type) -derive forall (t48 :: Type). (Ord1 (f :: Type -> Type), Ord ((g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)))) => Ord (Compose @Type @(t48 :: Type) (f :: Type -> Type) (g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)) :: Type) -derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) -derive Ord1 (f :: Type -> Type) => Ord1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) -derive Eq (a :: Type) => Eq (NoOrd (a :: Type) :: Type) -derive Eq1 (NoOrd :: Type -> Type) -derive Ord1 (NoOrd :: Type -> Type) derive Eq (a :: Type) => Eq (Id (a :: Type) :: Type) derive Eq1 (Id :: Type -> Type) derive Ord (a :: Type) => Ord (Id (a :: Type) :: Type) @@ -67,6 +60,13 @@ derive (Eq1 (f :: Type -> Type), Eq (a :: Type)) => Eq (Wrap @Type (f :: Type -> derive Eq1 (f :: Type -> Type) => Eq1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) derive (Ord1 (f :: Type -> Type), Ord (a :: Type)) => Ord (Wrap @Type (f :: Type -> Type) (a :: Type) :: Type) derive Ord1 (f :: Type -> Type) => Ord1 (Wrap @Type (f :: Type -> Type) :: Type -> Type) +derive forall (t40 :: Type). (Eq1 (f :: Type -> Type), Eq ((g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)))) => Eq (Compose @Type @(t40 :: Type) (f :: Type -> Type) (g :: (t40 :: Type) -> Type) (a :: (t40 :: Type)) :: Type) +derive forall (t48 :: Type). (Ord1 (f :: Type -> Type), Ord ((g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)))) => Ord (Compose @Type @(t48 :: Type) (f :: Type -> Type) (g :: (t48 :: Type) -> Type) (a :: (t48 :: Type)) :: Type) +derive Eq1 (f :: Type -> Type) => Eq1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Ord1 (f :: Type -> Type) => Ord1 (Compose @Type @Type (f :: Type -> Type) (g :: Type -> Type) :: Type -> Type) +derive Eq (a :: Type) => Eq (NoOrd (a :: Type) :: Type) +derive Eq1 (NoOrd :: Type -> Type) +derive Ord1 (NoOrd :: Type -> Type) Diagnostics error[NoInstanceFound]: No instance found for: Eq ((g :: Type -> Type) (~_ :: Type)) diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 102fc69c..3c858b23 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -14,130 +14,101 @@ use lowering::{ use rowan::ast::AstNode; use syntax::cst; -pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { - let resolved = engine.resolved(id).unwrap(); +macro_rules! pos { + ($content:expr, $stabilized:expr, $id:expr) => {{ + let cst = $stabilized.ast_ptr($id).unwrap(); + let range = cst.syntax_node_ptr().text_range(); + let p = locate::offset_to_position($content, range.start()).unwrap(); + format!("{}:{}", p.line, p.character) + }}; +} - let mut buffer = String::default(); - writeln!(buffer, "module {name}").unwrap(); +fn heading(out: &mut String, title: &str) { + writeln!(out).unwrap(); + writeln!(out, "{title}").unwrap(); +} - writeln!(buffer).unwrap(); - writeln!(buffer, "Unqualified Imports:").unwrap(); - for import in resolved.unqualified.values().flatten() { - writeln!(buffer).unwrap(); - writeln!(buffer, "Terms:").unwrap(); - for (name, _, _, kind) in import.iter_terms() { +macro_rules! write_import_items { + ($out:expr, $title:expr, $iter:expr) => {{ + writeln!($out).unwrap(); + writeln!($out, "{}:", $title).unwrap(); + for (item_name, _, _, kind) in $iter { if matches!(kind, ImportKind::Hidden) { continue; } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); + writeln!($out, " - {item_name} is {kind:?}").unwrap(); } + }}; +} - writeln!(buffer).unwrap(); - writeln!(buffer, "Types:").unwrap(); - for (name, _, _, kind) in import.iter_types() { - if matches!(kind, ImportKind::Hidden) { - continue; - } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } +pub fn report_resolved(engine: &QueryEngine, id: FileId, name: &str) -> String { + let resolved = engine.resolved(id).unwrap(); - writeln!(buffer).unwrap(); - writeln!(buffer, "Classes:").unwrap(); - for (name, _, _, kind) in import.iter_classes() { - if matches!(kind, ImportKind::Hidden) { - continue; - } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } + let mut out = String::default(); + writeln!(out, "module {name}").unwrap(); + + heading(&mut out, "Unqualified Imports:"); + for import in resolved.unqualified.values().flatten() { + write_import_items!(out, "Terms", import.iter_terms()); + write_import_items!(out, "Types", import.iter_types()); + write_import_items!(out, "Classes", import.iter_classes()); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Qualified Imports:").unwrap(); - for (name, imports) in &resolved.qualified { + heading(&mut out, "Qualified Imports:"); + for (qualifier, imports) in &resolved.qualified { for import in imports { - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Terms:").unwrap(); - for (name, _, _, kind) in import.iter_terms() { - if matches!(kind, ImportKind::Hidden) { - continue; - } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } - - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Types:").unwrap(); - for (name, _, _, kind) in import.iter_types() { - if matches!(kind, ImportKind::Hidden) { - continue; - } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } - - writeln!(buffer).unwrap(); - writeln!(buffer, "{name} Classes:").unwrap(); - for (name, _, _, kind) in import.iter_classes() { - if matches!(kind, ImportKind::Hidden) { - continue; - } - writeln!(buffer, " - {name} is {kind:?}").unwrap(); - } + write_import_items!(out, format!("{qualifier} Terms"), import.iter_terms()); + write_import_items!(out, format!("{qualifier} Types"), import.iter_types()); + write_import_items!(out, format!("{qualifier} Classes"), import.iter_classes()); } } - writeln!(buffer).unwrap(); - writeln!(buffer, "Exported Terms:").unwrap(); + heading(&mut out, "Exported Terms:"); for (name, _, _) in resolved.exports.iter_terms() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Exported Types:").unwrap(); + heading(&mut out, "Exported Types:"); for (name, _, _) in resolved.exports.iter_types() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Exported Classes:").unwrap(); + heading(&mut out, "Exported Classes:"); for (name, _, _) in resolved.exports.iter_classes() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Local Terms:").unwrap(); + heading(&mut out, "Local Terms:"); for (name, _, _) in resolved.locals.iter_terms() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Local Types:").unwrap(); + heading(&mut out, "Local Types:"); for (name, _, _) in resolved.locals.iter_types() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Local Classes:").unwrap(); + heading(&mut out, "Local Classes:"); for (name, _, _) in resolved.locals.iter_classes() { - writeln!(buffer, " - {name}").unwrap(); + writeln!(out, " - {name}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Class Members:").unwrap(); + heading(&mut out, "Class Members:"); let indexed = engine.indexed(id).unwrap(); let mut class_member_entries: Vec<_> = resolved.class.iter().collect(); class_member_entries.sort_by_key(|(class_id, name, _, _)| (class_id.into_raw(), name.as_str())); for (class_id, member_name, member_file, _) in class_member_entries { let class_name = resolve_class_name(engine, &indexed, id, (member_file, class_id)); let locality = if member_file == id { "" } else { " (imported)" }; - writeln!(buffer, " - {class_name}.{member_name}{locality}").unwrap(); + writeln!(out, " - {class_name}.{member_name}{locality}").unwrap(); } - writeln!(buffer).unwrap(); - writeln!(buffer, "Errors:").unwrap(); + heading(&mut out, "Errors:"); for error in &resolved.errors { - writeln!(buffer, " - {error:?}").unwrap(); + writeln!(out, " - {error:?}").unwrap(); } - buffer + out } pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { @@ -151,35 +122,35 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { let info = &lowered.info; let graph = &lowered.graph; - let mut buffer = String::default(); - writeln!(buffer, "module {name}").unwrap(); + let mut out = String::default(); + writeln!(out, "module {name}").unwrap(); - writeln!(buffer).unwrap(); - writeln!(buffer, "Expressions:").unwrap(); - writeln!(buffer).unwrap(); + writeln!(out).unwrap(); + writeln!(out, "Expressions:").unwrap(); + writeln!(out).unwrap(); for (expression_id, _) in info.iter_expression() { let Some(kind) = info.get_expression_kind(expression_id) else { continue; }; if let ExpressionKind::Variable { resolution, .. } = kind { - report_on_term( + write_term_resolution( &content, &stabilized, &module, info, - &mut buffer, + &mut out, expression_id, resolution, ); } else if let ExpressionKind::Record { record } = kind { for field in record.iter() { if let lowering::ExpressionRecordItem::RecordPun { resolution, .. } = field { - report_on_term( + write_term_resolution( &content, &stabilized, &module, info, - &mut buffer, + &mut out, expression_id, resolution, ); @@ -190,16 +161,7 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { } } - writeln!(buffer, "\nTypes:\n").unwrap(); - - macro_rules! pos { - ($id:expr) => {{ - let cst = stabilized.ast_ptr($id).unwrap(); - let range = cst.syntax_node_ptr().text_range(); - let p = locate::offset_to_position(&content, range.start()).unwrap(); - format!("{}:{}", p.line, p.character) - }}; - } + writeln!(out, "\nTypes:\n").unwrap(); for (type_id, _) in info.iter_type() { let Some(TypeKind::Variable { resolution, .. }) = info.get_type_kind(type_id) else { @@ -210,42 +172,143 @@ pub fn report_lowered(engine: &QueryEngine, id: FileId, name: &str) -> String { let node = cst.syntax_node_ptr().to_node(module.syntax()); let text = node.text().to_string(); - writeln!(buffer, "{}@{}", text.trim(), pos!(type_id)).unwrap(); + writeln!(out, "{}@{}", text.trim(), pos!(&content, &stabilized, type_id)).unwrap(); match resolution { Some(TypeVariableResolution::Forall(id)) => { - writeln!(buffer, " -> forall@{}", pos!(*id)).unwrap(); + writeln!(out, " -> forall@{}", pos!(&content, &stabilized, *id)).unwrap(); } Some(TypeVariableResolution::Implicit(ImplicitTypeVariable { binding, node, id })) => { let GraphNode::Implicit { bindings, .. } = &graph[*node] else { - writeln!(buffer, " did not resolve to constraint variable!").unwrap(); + writeln!(out, " did not resolve to constraint variable!").unwrap(); continue; }; let (name, type_ids) = bindings.get_index(*id).expect("invariant violated: invalid index"); if *binding { - writeln!(buffer, " introduces a constraint variable {name:?}").unwrap(); + writeln!(out, " introduces a constraint variable {name:?}").unwrap(); } else { - writeln!(buffer, " -> constraint variable {name:?}").unwrap(); + writeln!(out, " -> constraint variable {name:?}").unwrap(); for &tid in type_ids { - writeln!(buffer, " {}", pos!(tid)).unwrap(); + writeln!(out, " {}", pos!(&content, &stabilized, tid)).unwrap(); } } } None => { - writeln!(buffer, " -> nothing").unwrap(); + writeln!(out, " -> nothing").unwrap(); } } } - buffer + out +} + +pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { + let indexed = engine.indexed(id).unwrap(); + let checked = engine.checked(id).unwrap(); + + let mut out = String::default(); + + write_checked_output(&mut out, engine, id, &indexed, &checked); + write_checked_diagnostics(&mut out, engine, id, &indexed, &checked); + + out +} + +pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { + let indexed = engine.indexed(id).unwrap(); + let checked = engine.checked2(id).unwrap(); + + let mut out = String::default(); + + writeln!(out, "Terms").unwrap(); + for (id, TermItem { name, .. }) in indexed.items.iter_terms() { + let Some(name) = name else { continue }; + let Some(kind) = checked.lookup_term(id) else { continue }; + let signature = pretty2::Pretty::new(engine).signature(name).render(kind); + writeln!(out, "{signature}").unwrap(); + } + + writeln!(out, "\nTypes").unwrap(); + for (id, TypeItem { name, .. }) in indexed.items.iter_types() { + let Some(name) = name else { continue }; + let Some(kind) = checked.lookup_type(id) else { continue }; + let signature = pretty2::Pretty::new(engine).signature(name).render(kind); + writeln!(out, "{signature}").unwrap(); + } + + if !checked.synonyms.is_empty() { + writeln!(out, "\nSynonyms").unwrap(); + } + for (id, TypeItem { name, .. }) in indexed.items.iter_types() { + let Some(name) = name else { continue }; + let Some(definition) = checked.lookup_synonym(id) else { continue }; + let names = definition.parameters.iter().map(|b| (b.name, b.text.clone())); + let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); + let binders = definition.parameters.iter().map(|b| b.text.as_str()).collect_vec(); + let binders_formatted = + if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; + writeln!(out, "type {name}{binders_formatted} = {replacement}").unwrap(); + } + + if !checked.classes.is_empty() { + writeln!(out, "\nClasses").unwrap(); + } + for (id, TypeItem { .. }) in indexed.items.iter_types() { + let Some(class) = checked.lookup_class(id) else { continue }; + + let canonical = pretty2::Pretty::new(engine).render(class.canonical); + + let mut superclasses = String::new(); + if !class.superclasses.is_empty() { + let formatted = class + .superclasses + .iter() + .map(|&superclass| pretty2::Pretty::new(engine).render(superclass)) + .collect_vec(); + superclasses = format!(" <= {}", formatted.join(", ")); + } + + writeln!(out, "class {canonical}{superclasses}").unwrap(); + for &mid in &class.members { + let Some(member_name) = indexed.items[mid].name.as_deref() else { continue }; + let Some(member_type) = checked.lookup_term(mid) else { continue }; + let signature = pretty2::Pretty::new(engine).signature(member_name).render(member_type); + writeln!(out, " {signature}").unwrap(); + } + } + + if !checked.roles.is_empty() { + writeln!(out, "\nRoles").unwrap(); + } + for (id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { + let (TypeItemKind::Data { .. } + | TypeItemKind::Newtype { .. } + | TypeItemKind::Foreign { .. }) = kind + else { + continue; + }; + let Some(name) = name else { continue }; + let Some(roles) = checked.lookup_roles(id) else { continue }; + let roles_str: Vec<_> = roles.iter().map(|r| format!("{r:?}")).collect(); + writeln!(out, "{name} = [{}]", roles_str.join(", ")).unwrap(); + } + + if !checked.errors.is_empty() { + writeln!(out, "\nErrors").unwrap(); + } + for error in &checked.errors { + writeln!(out, "{error:#?}").unwrap(); + } + + out } -fn report_on_term( +fn write_term_resolution( content: &str, stabilized: &stabilizing::StabilizedModule, module: &cst::Module, info: &lowering::LoweringInfo, - buffer: &mut String, + out: &mut String, expression_id: lowering::ExpressionId, resolution: &Option, ) { @@ -254,80 +317,72 @@ fn report_on_term( let text = node.text().to_string(); let position = locate::offset_to_position(content, node.text_range().start()).unwrap(); - writeln!(buffer, "{}@{}:{}", text.trim(), position.line, position.character).unwrap(); - - macro_rules! pos { - ($id:expr) => {{ - let cst = stabilized.ast_ptr($id).unwrap(); - let range = cst.syntax_node_ptr().text_range(); - let p = locate::offset_to_position(content, range.start()).unwrap(); - format!("{}:{}", p.line, p.character) - }}; - } + writeln!(out, "{}@{}:{}", text.trim(), position.line, position.character).unwrap(); match resolution { Some(TermVariableResolution::Binder(id)) => { - writeln!(buffer, " -> binder@{}", pos!(*id)).unwrap(); + writeln!(out, " -> binder@{}", pos!(content, stabilized, *id)).unwrap(); } Some(TermVariableResolution::Let(let_binding_id)) => { let let_binding = info.get_let_binding_group(*let_binding_id); if let Some(sig) = let_binding.signature { - writeln!(buffer, " -> signature@{}", pos!(sig)).unwrap(); + writeln!(out, " -> signature@{}", pos!(content, stabilized, sig)).unwrap(); } for eq in let_binding.equations.iter() { - writeln!(buffer, " -> equation@{}", pos!(*eq)).unwrap(); + writeln!(out, " -> equation@{}", pos!(content, stabilized, *eq)).unwrap(); } } Some(TermVariableResolution::RecordPun(id)) => { - writeln!(buffer, " -> record pun@{}", pos!(*id)).unwrap(); + writeln!(out, " -> record pun@{}", pos!(content, stabilized, *id)).unwrap(); } Some(TermVariableResolution::Reference(..)) => { - writeln!(buffer, " -> top-level").unwrap(); + writeln!(out, " -> top-level").unwrap(); } None => { - writeln!(buffer, " -> nothing").unwrap(); + writeln!(out, " -> nothing").unwrap(); } } } -pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { - let indexed = engine.indexed(id).unwrap(); - let checked = engine.checked(id).unwrap(); - - let mut snapshot = String::default(); - - writeln!(snapshot, "Terms").unwrap(); +fn write_checked_output( + out: &mut String, + engine: &QueryEngine, + id: FileId, + indexed: &indexing::IndexedModule, + checked: &checking::CheckedModule, +) { + writeln!(out, "Terms").unwrap(); for (id, TermItem { name, .. }) in indexed.items.iter_terms() { let Some(n) = name else { continue }; let Some(t) = checked.lookup_term(id) else { continue }; let signature = pretty::print_signature_global(engine, n, t); - writeln!(snapshot, "{signature}").unwrap(); + writeln!(out, "{signature}").unwrap(); } - writeln!(snapshot, "\nTypes").unwrap(); + writeln!(out, "\nTypes").unwrap(); for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(n) = name else { continue }; let Some(t) = checked.lookup_type(id) else { continue }; let signature = pretty::print_signature_global(engine, n, t); - writeln!(snapshot, "{signature}").unwrap(); + writeln!(out, "{signature}").unwrap(); } if !checked.synonyms.is_empty() { - writeln!(snapshot, "\nSynonyms").unwrap(); + writeln!(out, "\nSynonyms").unwrap(); } for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(group) = checked.lookup_synonym(id) else { continue }; let synonym = pretty::print_global(engine, group.synonym_type); - writeln!(snapshot, "{name} = {synonym}").unwrap(); - writeln!(snapshot, " Quantified = {}", group.quantified_variables).unwrap(); - writeln!(snapshot, " Kind = {}", group.kind_variables).unwrap(); - writeln!(snapshot, " Type = {}", group.type_variables).unwrap(); - writeln!(snapshot).unwrap(); + writeln!(out, "{name} = {synonym}").unwrap(); + writeln!(out, " Quantified = {}", group.quantified_variables).unwrap(); + writeln!(out, " Kind = {}", group.kind_variables).unwrap(); + writeln!(out, " Type = {}", group.type_variables).unwrap(); + writeln!(out).unwrap(); } if !checked.data.is_empty() { - writeln!(snapshot, "\nData").unwrap(); + writeln!(out, "\nData").unwrap(); } for (id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { let (TypeItemKind::Data { .. } | TypeItemKind::Newtype { .. }) = kind else { @@ -335,14 +390,14 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { }; let Some(name) = name else { continue }; let Some(data) = checked.lookup_data(id) else { continue }; - writeln!(snapshot, "{name}").unwrap(); - writeln!(snapshot, " Quantified = {}", data.quantified_variables).unwrap(); - writeln!(snapshot, " Kind = {}", data.kind_variables).unwrap(); - writeln!(snapshot).unwrap(); + writeln!(out, "{name}").unwrap(); + writeln!(out, " Quantified = {}", data.quantified_variables).unwrap(); + writeln!(out, " Kind = {}", data.kind_variables).unwrap(); + writeln!(out).unwrap(); } if !checked.roles.is_empty() { - writeln!(snapshot, "\nRoles").unwrap(); + writeln!(out, "\nRoles").unwrap(); } for (id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { let (TypeItemKind::Data { .. } @@ -354,11 +409,11 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { let Some(name) = name else { continue }; let Some(roles) = checked.lookup_roles(id) else { continue }; let roles_str: Vec<_> = roles.iter().map(|r| format!("{r:?}")).collect(); - writeln!(snapshot, "{name} = [{}]", roles_str.join(", ")).unwrap(); + writeln!(out, "{name} = [{}]", roles_str.join(", ")).unwrap(); } if !checked.classes.is_empty() { - writeln!(snapshot, "\nClasses").unwrap(); + writeln!(out, "\nClasses").unwrap(); } for (type_id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { let TypeItemKind::Class { .. } = kind else { continue }; @@ -367,7 +422,6 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { let mut class_line = String::new(); - // Print superclasses first (before <=) if !class.superclasses.is_empty() { for (i, (superclass_type, _)) in class.superclasses.iter().enumerate() { if i > 0 { @@ -381,56 +435,60 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { class_line.push_str(name); - // Print class type variables with their kinds. for (name, &kind) in class.type_variable_names.iter().zip(class.type_variable_kinds.iter()) { let kind_str = pretty::print_global(engine, kind); class_line.push_str(&format!(" ({} :: {kind_str})", name.text)); } - writeln!(snapshot, "class {class_line}").unwrap(); + writeln!(out, "class {class_line}").unwrap(); } if !checked.instances.is_empty() { - writeln!(snapshot, "\nInstances").unwrap(); + writeln!(out, "\nInstances").unwrap(); } let mut instance_entries: Vec<_> = checked.instances.iter().collect(); - instance_entries.sort_by_key(|(id, _)| format!("{:?}", id)); + instance_entries.sort_by_key(|(id, _)| *id); for (_instance_id, instance) in instance_entries { - let class_name = resolve_class_name(engine, &indexed, id, instance.resolution); + let class_name = resolve_class_name(engine, indexed, id, instance.resolution); let head = format_instance_head(engine, &class_name, &instance.constraints, &instance.arguments); let forall_prefix = format_forall_prefix(engine, &instance.kind_variables); - writeln!(snapshot, "instance {forall_prefix}{head}").unwrap(); + writeln!(out, "instance {forall_prefix}{head}").unwrap(); if let checking::core::InstanceKind::Chain { position, .. } = instance.kind { - writeln!(snapshot, " chain: {}", position).unwrap(); + writeln!(out, " chain: {}", position).unwrap(); } } if !checked.derived.is_empty() { - writeln!(snapshot, "\nDerived").unwrap(); + writeln!(out, "\nDerived").unwrap(); } let mut derived_entries: Vec<_> = checked.derived.iter().collect(); - derived_entries.sort_by_key(|(id, _)| format!("{:?}", id)); + derived_entries.sort_by_key(|(id, _)| *id); for (_derive_id, instance) in derived_entries { - let class_name = resolve_class_name(engine, &indexed, id, instance.resolution); + let class_name = resolve_class_name(engine, indexed, id, instance.resolution); let head = format_instance_head(engine, &class_name, &instance.constraints, &instance.arguments); let forall_prefix = format_forall_prefix(engine, &instance.kind_variables); - writeln!(snapshot, "derive {forall_prefix}{head}").unwrap(); + writeln!(out, "derive {forall_prefix}{head}").unwrap(); } +} +fn write_checked_diagnostics( + out: &mut String, + engine: &QueryEngine, + id: FileId, + indexed: &indexing::IndexedModule, + checked: &checking::CheckedModule, +) { let content = engine.content(id); - let (parsed, _) = engine.parsed(id).unwrap(); let root = parsed.syntax_node(); - let stabilized = engine.stabilized(id).unwrap(); let lowered = engine.lowered(id).unwrap(); let resolved = engine.resolved(id).unwrap(); - let context = - DiagnosticsContext::new(&content, &root, &stabilized, &indexed, &lowered, &checked); + let context = DiagnosticsContext::new(&content, &root, &stabilized, indexed, &lowered, checked); let mut all_diagnostics = vec![]; @@ -447,109 +505,9 @@ pub fn report_checked(engine: &QueryEngine, id: FileId) -> String { } if !all_diagnostics.is_empty() { - writeln!(snapshot, "\nDiagnostics").unwrap(); - snapshot.push_str(&format_rustc(&all_diagnostics, &content)); - } - - snapshot -} - -pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { - let indexed = engine.indexed(id).unwrap(); - let checked = engine.checked2(id).unwrap(); - - let mut snapshot = String::default(); - - writeln!(snapshot, "Terms").unwrap(); - for (id, TermItem { name, .. }) in indexed.items.iter_terms() { - let Some(name) = name else { continue }; - let Some(kind) = checked.lookup_term(id) else { continue }; - let signature = pretty2::Pretty::new(engine).signature(name).render(kind); - writeln!(snapshot, "{signature}").unwrap(); - } - - writeln!(snapshot, "\nTypes").unwrap(); - for (id, TypeItem { name, .. }) in indexed.items.iter_types() { - let Some(name) = name else { continue }; - let Some(kind) = checked.lookup_type(id) else { continue }; - let signature = pretty2::Pretty::new(engine).signature(name).render(kind); - writeln!(snapshot, "{signature}").unwrap(); - } - - if !checked.synonyms.is_empty() { - writeln!(snapshot, "\nSynonyms").unwrap(); + writeln!(out, "\nDiagnostics").unwrap(); + out.push_str(&format_rustc(&all_diagnostics, &content)); } - for (id, TypeItem { name, .. }) in indexed.items.iter_types() { - let Some(name) = name else { continue }; - let Some(definition) = checked.lookup_synonym(id) else { continue }; - let names = definition.parameters.iter().map(|b| (b.name, b.text.clone())); - let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); - let binders = definition.parameters.iter().map(|b| b.text.as_str()).collect_vec(); - let binders_formatted = - if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; - writeln!(snapshot, "type {name}{binders_formatted} = {replacement}").unwrap(); - } - - if !checked.classes.is_empty() { - writeln!(snapshot, "\nClasses").unwrap(); - } - for (id, TypeItem { .. }) in indexed.items.iter_types() { - let Some(class) = checked.lookup_class(id) else { continue }; - - let canonical = pretty2::Pretty::new(engine).render(class.canonical); - - let mut superclasses = String::new(); - if !class.superclasses.is_empty() { - let formatted = class - .superclasses - .iter() - .map(|&superclass| pretty2::Pretty::new(engine).render(superclass)) - .collect_vec(); - superclasses = format!(" <= {}", formatted.join(", ")); - } - - let members = class - .members - .iter() - .filter_map(|&mid| { - let member_name = indexed.items[mid].name.as_deref()?; - let member_type = checked.lookup_term(mid)?; - let signature = - pretty2::Pretty::new(engine).signature(member_name).render(member_type); - Some(format!(" {signature}")) - }) - .collect_vec(); - - writeln!(snapshot, "class {canonical}{superclasses}").unwrap(); - for member in &members { - writeln!(snapshot, "{member}").unwrap(); - } - } - - if !checked.roles.is_empty() { - writeln!(snapshot, "\nRoles").unwrap(); - } - for (id, TypeItem { name, kind, .. }) in indexed.items.iter_types() { - let (TypeItemKind::Data { .. } - | TypeItemKind::Newtype { .. } - | TypeItemKind::Foreign { .. }) = kind - else { - continue; - }; - let Some(name) = name else { continue }; - let Some(roles) = checked.lookup_roles(id) else { continue }; - let roles_str: Vec<_> = roles.iter().map(|r| format!("{r:?}")).collect(); - writeln!(snapshot, "{name} = [{}]", roles_str.join(", ")).unwrap(); - } - - if !checked.errors.is_empty() { - writeln!(snapshot, "\nErrors").unwrap(); - } - for error in &checked.errors { - writeln!(snapshot, "{error:#?}").unwrap(); - } - - snapshot } fn resolve_class_name( From 27fab9a4948a37741f1c9515b12fcc70cb12353a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 01:37:12 +0800 Subject: [PATCH 256/386] Use SmolStrId for ForallBinder::text --- compiler-core/checking2/src/core.rs | 4 ++-- .../checking2/src/core/generalise.rs | 2 +- compiler-core/checking2/src/core/pretty.rs | 5 +++-- compiler-core/checking2/src/interners.rs | 2 +- compiler-core/checking2/src/source.rs | 20 ++++++++++++------- compiler-core/checking2/src/source/types.rs | 4 +++- .../015_operator_alias_invalid_kind/Main.snap | 2 +- .../Main.snap | 4 ++-- tests-integration/src/generated/basic.rs | 7 +++++-- 9 files changed, 31 insertions(+), 19 deletions(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index f50c8b49..6682c39d 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -45,14 +45,14 @@ impl Depth { } /// Carries information about a type variable under a forall. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ForallBinder { /// Whether this binder is visible to type applications. pub visible: bool, /// The unique identity attached to the type variable. pub name: Name, /// The source-level text of the type variable. - pub text: SmolStr, + pub text: SmolStrId, /// The kind of the type variable. pub kind: TypeId, } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index d366653d..50a50ec4 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -172,7 +172,7 @@ where let UnificationEntry { kind, .. } = *state.unifications.get(unification_id); let name = state.names.fresh(); - let text = name.as_text(); + let text = context.queries.intern_smol_str(name.as_text()); let binder = ForallBinder { visible: false, name, text, kind }; let binder = context.intern_forall_binder(binder); diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 8f74dd87..7f93ae11 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -361,15 +361,16 @@ where // Register source-level names so rigid variables in the body // display their original names instead of synthetic ones. for binder in &binders { - self.names.insert(binder.name, SmolStr::clone(&binder.text)); + self.names.insert(binder.name, self.lookup_smol_str(binder.text)); } let binders = binders .iter() .map(|binder| { + let text = self.lookup_smol_str(binder.text); let kind = self.traverse(Precedence::Top, binder.kind); self.arena - .text(format!("({} :: ", binder.text)) + .text(format!("({} :: ", text)) .append(kind) .append(self.arena.text(")")) .group() diff --git a/compiler-core/checking2/src/interners.rs b/compiler-core/checking2/src/interners.rs index 0412d638..c68e180c 100644 --- a/compiler-core/checking2/src/interners.rs +++ b/compiler-core/checking2/src/interners.rs @@ -29,7 +29,7 @@ impl CoreInterners { } pub fn lookup_forall_binder(&self, id: ForallBinderId) -> ForallBinder { - self.forall_binders[id].clone() + self.forall_binders[id] } pub fn intern_row_type(&mut self, r: RowType) -> RowTypeId { diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 8e9e4884..c2c9f8f1 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -17,6 +17,7 @@ use lowering::{ ClassIr, DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, TypeItemIr, TypeVariableBinding, }; +use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; @@ -322,7 +323,13 @@ where let name = state.names.fresh(); state.kind_scope.bind_forall(equation_binding.id, name, kind); - let text = equation_binding.name.clone().unwrap_or_else(|| name.as_text()); + let text = if let Some(name) = &equation_binding.name { + SmolStr::clone(name) + } else { + name.as_text() + }; + + let text = context.queries.intern_smol_str(text); let visible = equation_binding.visible; binders.push(ForallBinder { visible, name, text, kind }); @@ -481,8 +488,7 @@ where for (index, parameter) in parameters.iter().enumerate().rev() { let kind = get_parameter_kind(index); - let parameter = ForallBinder::clone(parameter); - let binder = ForallBinder { kind, ..parameter }; + let binder = ForallBinder { kind, ..*parameter }; let binder_id = context.intern_forall_binder(binder); result = context.intern_forall(binder_id, result); @@ -490,7 +496,7 @@ where // forall (k :: Type) (t :: k) (a :: Type). a -> Tagged @k t a for binder in kind_binders.iter().rev() { - let binder_id = context.intern_forall_binder(binder.clone()); + let binder_id = context.intern_forall_binder(*binder); result = context.intern_forall(binder_id, result); } @@ -719,13 +725,13 @@ where let kind_binders = class_binders .iter() - .cloned() + .copied() .map(|binder| context.intern_forall_binder(binder)) .collect_vec(); let type_parameters = parameters .iter() - .cloned() + .copied() .enumerate() .map(|(index, parameter)| { let kind = get_parameter_kind(index); @@ -760,7 +766,7 @@ where let mut result = context.intern_constrained(canonical, member_inner); - for member_binder in member_binders.iter().cloned().rev() { + for member_binder in member_binders.iter().copied().rev() { let binder_id = context.intern_forall_binder(member_binder); result = context.intern_forall(binder_id, result); } diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 13bb1e98..748bbd79 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -170,7 +170,7 @@ where }; let t = binders.iter().rfold(inner, |inner, binder| { - let binder_id = context.intern_forall_binder(binder.clone()); + let binder_id = context.intern_forall_binder(*binder); context.intern_forall(binder_id, inner) }); @@ -362,7 +362,9 @@ where let visible = binding.visible; let name = state.names.fresh(); + let text = if let Some(name) = &binding.name { SmolStr::clone(name) } else { name.as_text() }; + let text = context.queries.intern_smol_str(text); state.kind_scope.bind_forall(binding.id, name, kind); Ok(ForallBinder { visible, name, text, kind }) diff --git a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap index e79e718a..cc51886b 100644 --- a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap +++ b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap @@ -15,7 +15,7 @@ type Identity a = (a :: (t1 :: Type)) Errors CheckError { kind: InvalidTypeOperator { - kind_message: Id(3), + kind_message: Id(5), }, crumbs: [], } diff --git a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap index e9b84989..9110e3e4 100644 --- a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap +++ b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap @@ -17,8 +17,8 @@ type Bad = Int + "hello" Errors CheckError { kind: CannotUnify { - t1: Id(5), - t2: Id(6), + t1: Id(7), + t2: Id(8), }, crumbs: [ CheckingKind( diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 3c858b23..481b567c 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; +use checking2::ExternalQueries; use checking2::core::pretty as pretty2; use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; @@ -242,9 +243,11 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(definition) = checked.lookup_synonym(id) else { continue }; - let names = definition.parameters.iter().map(|b| (b.name, b.text.clone())); + let names = + definition.parameters.iter().map(|b| (b.name, engine.lookup_smol_str(b.text))); let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); - let binders = definition.parameters.iter().map(|b| b.text.as_str()).collect_vec(); + let binders = + definition.parameters.iter().map(|b| engine.lookup_smol_str(b.text)).collect_vec(); let binders_formatted = if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; writeln!(out, "type {name}{binders_formatted} = {replacement}").unwrap(); From 6efbb195aaadfc72ecd1803b5cbf11ebb29ffc0c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 02:58:57 +0800 Subject: [PATCH 257/386] Implement basic role inference --- compiler-core/checking2/src/source.rs | 59 ++++- compiler-core/checking2/src/source/roles.rs | 250 ++++++++++++++++++ .../checking2/001_foreign_check/Main.snap | 5 + .../checking2/003_data_check/Main.snap | 3 + .../checking2/004_data_infer/Main.snap | 3 + .../checking2/005_newtype_check/Main.snap | 3 + .../checking2/006_newtype_infer/Main.snap | 3 + 7 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 compiler-core/checking2/src/source/roles.rs diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index c2c9f8f1..8cba731b 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -1,6 +1,7 @@ //! Implements syntax-driven checking rules for source files. pub mod operator; +pub mod roles; pub mod signature; pub mod synonym; pub mod terms; @@ -22,8 +23,8 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, Type, TypeId, generalise, toolkit, unification, - zonk, + CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, generalise, toolkit, + unification, zonk, }; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; @@ -31,6 +32,7 @@ use crate::state::CheckState; struct PendingDataType { parameters: Vec, constructors: Vec<(TermItemId, Vec)>, + declared_roles: Arc<[lowering::Role]>, } struct PendingSynonymType { @@ -51,6 +53,7 @@ struct TypeSccState { data: Vec<(TypeItemId, PendingDataType)>, synonym: Vec<(TypeItemId, PendingSynonymType)>, class: Vec<(TypeItemId, PendingClassType)>, + foreign: Vec<(TypeItemId, Arc<[lowering::Role]>)>, operator: Vec, } @@ -87,6 +90,7 @@ where } finalise_binding_group(state, context, &items)?; + finalise_roles(state, context, &mut scc_state)?; finalise_data_constructors(state, context, &mut scc_state)?; finalise_synonym_replacements(state, context, &mut scc_state)?; finalise_classes(state, context, &mut scc_state)?; @@ -241,13 +245,13 @@ where }; match item { - TypeItemIr::DataGroup { signature, data, .. } => { + TypeItemIr::DataGroup { signature, data, roles } => { let Some(DataIr { variables }) = data else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, variables)?; + check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; } - TypeItemIr::NewtypeGroup { signature, newtype, .. } => { + TypeItemIr::NewtypeGroup { signature, newtype, roles } => { let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, variables)?; + check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; } TypeItemIr::SynonymGroup { signature, synonym, .. } => { let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; @@ -257,7 +261,9 @@ where let Some(class) = class else { return Ok(()) }; check_class_equation(state, context, scc, item_id, *signature, class)?; } - TypeItemIr::Foreign { .. } => {} + TypeItemIr::Foreign { roles, .. } => { + scc.foreign.push((item_id, Arc::clone(roles))); + } TypeItemIr::Operator { resolution, .. } => { check_operator_equation(state, context, scc, item_id, *resolution)?; } @@ -273,6 +279,7 @@ fn check_data_equation( item_id: TypeItemId, signature: Option, variables: &[TypeVariableBinding], + declared_roles: &Arc<[lowering::Role]>, ) -> QueryResult<()> where Q: ExternalQueries, @@ -286,7 +293,9 @@ where }; let constructors = check_data_constructors(state, context, item_id)?; - scc.data.push((item_id, PendingDataType { parameters, constructors })); + let declared_roles = Arc::clone(declared_roles); + + scc.data.push((item_id, PendingDataType { parameters, constructors, declared_roles })); Ok(()) } @@ -431,7 +440,7 @@ fn finalise_data_constructors( where Q: ExternalQueries, { - for (item_id, PendingDataType { parameters, constructors }) in mem::take(&mut scc.data) { + for (item_id, PendingDataType { parameters, constructors, .. }) in mem::take(&mut scc.data) { // constructor_kind should have already been generalised by the // finalise_binding_group function. the kind signature is used // as the source of truth for constructing the kind applications @@ -507,6 +516,38 @@ where Ok(()) } +fn finalise_roles( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, pending) in &scc.data { + let PendingDataType { parameters, constructors, declared_roles } = pending; + let inferred_roles = roles::infer_data_roles(state, context, parameters, constructors)?; + let resolved_roles = + roles::check_declared_roles(state, *item_id, &inferred_roles, declared_roles, false); + state.checked.roles.insert(*item_id, resolved_roles); + } + + for (item_id, declared_roles) in mem::take(&mut scc.foreign) { + let Some(kind) = state.checked.lookup_type(item_id) else { + continue; + }; + + let parameter_count = roles::count_kind_arguments(state, context, kind)?; + let inferred_roles = vec![Role::Nominal; parameter_count]; + let resolved_roles = + roles::check_declared_roles(state, item_id, &inferred_roles, &declared_roles, true); + + state.checked.roles.insert(item_id, resolved_roles); + } + + Ok(()) +} + fn check_synonym_equation( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs new file mode 100644 index 00000000..0afe5073 --- /dev/null +++ b/compiler-core/checking2/src/source/roles.rs @@ -0,0 +1,250 @@ +//! Implements role checking for source type declarations. + +use std::sync::Arc; + +use building_types::QueryResult; +use indexing::{TermItemId, TypeItemId}; +use rustc_hash::FxHashMap; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{ForallBinder, Name, Role, Type, TypeId, normalise, toolkit}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::state::CheckState; + +pub fn infer_data_roles( + state: &mut CheckState, + context: &CheckContext, + parameters: &[ForallBinder], + constructors: &[(TermItemId, Vec)], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut inference = RoleInference::new(state, context, parameters); + + for (_, arguments) in constructors { + for &argument in arguments { + infer_roles(&mut inference, argument, RoleInferencePosition::ROOT)?; + } + } + + Ok(inference.finish()) +} + +pub fn count_kind_arguments( + state: &mut CheckState, + context: &CheckContext, + kind: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, kind)?; + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, quantified)?; + Ok(arguments.len()) +} + +pub fn check_declared_roles( + state: &mut CheckState, + item_id: TypeItemId, + inferred: &[Role], + declared: &[lowering::Role], + is_foreign: bool, +) -> Arc<[Role]> { + let mut validated = inferred.to_vec(); + + for (index, (&inferred_role, declared_role)) in inferred.iter().zip(declared.iter()).enumerate() + { + let Some(declared_role) = lower_role(declared_role) else { + continue; + }; + + if is_foreign || declared_role >= inferred_role { + validated[index] = declared_role; + } else { + state.with_error_crumb(ErrorCrumb::TypeDeclaration(item_id), |state| { + state.insert_error(ErrorKind::InvalidRoleDeclaration { + index, + declared: declared_role, + inferred: inferred_role, + }); + }); + } + } + + Arc::from(validated) +} + +fn lower_role(role: &lowering::Role) -> Option { + match role { + lowering::Role::Nominal => Some(Role::Nominal), + lowering::Role::Representational => Some(Role::Representational), + lowering::Role::Phantom => Some(Role::Phantom), + lowering::Role::Unknown => None, + } +} + +#[derive(Copy, Clone, Debug)] +struct RoleInferencePosition { + under_constraint: bool, + variable_argument: bool, +} + +impl RoleInferencePosition { + const ROOT: RoleInferencePosition = + RoleInferencePosition { under_constraint: false, variable_argument: false }; + + fn child(self) -> RoleInferencePosition { + RoleInferencePosition { under_constraint: self.under_constraint, variable_argument: false } + } + + fn constrained_child(self) -> RoleInferencePosition { + RoleInferencePosition { under_constraint: true, variable_argument: false } + } + + fn argument_child(self, is_variable_argument: bool) -> RoleInferencePosition { + RoleInferencePosition { + under_constraint: self.under_constraint, + variable_argument: is_variable_argument, + } + } + + fn requires_nominal_role(self) -> bool { + self.under_constraint || self.variable_argument + } +} + +struct RoleInference<'a, 'q, Q> +where + Q: ExternalQueries, +{ + state: &'a mut CheckState, + context: &'a CheckContext<'q, Q>, + roles: Vec, + parameters: FxHashMap, +} + +impl<'a, 'q, Q> RoleInference<'a, 'q, Q> +where + Q: ExternalQueries, +{ + fn new( + state: &'a mut CheckState, + context: &'a CheckContext<'q, Q>, + parameters: &[ForallBinder], + ) -> RoleInference<'a, 'q, Q> { + let parameter_count = parameters.len(); + let parameters: FxHashMap = parameters + .iter() + .enumerate() + .map(|(index, parameter)| (parameter.name, index)) + .collect(); + + RoleInference { state, context, roles: vec![Role::Phantom; parameter_count], parameters } + } + + fn finish(self) -> Vec { + self.roles + } +} + +fn infer_roles<'a, 'q, Q>( + inference: &mut RoleInference<'a, 'q, Q>, + type_id: TypeId, + mode: RoleInferencePosition, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let type_id = normalise::normalise(inference.state, inference.context, type_id)?; + + match inference.context.lookup_type(type_id) { + Type::Rigid(name, _, kind) => { + if let Some(index) = inference.parameters.get(&name) { + let role = if mode.requires_nominal_role() { + Role::Nominal + } else { + Role::Representational + }; + inference.roles[*index] = inference.roles[*index].max(role); + } + + infer_roles(inference, kind, mode.child())?; + } + + Type::Application(function, argument) => { + let function_id = normalise::normalise(inference.state, inference.context, function)?; + + let is_type_variable = + matches!(inference.context.lookup_type(function_id), Type::Rigid(_, _, _)); + + infer_roles(inference, function, mode.child())?; + infer_roles(inference, argument, mode.argument_child(is_type_variable))?; + } + + Type::Constrained(constraint, inner) => { + let mode = mode.constrained_child(); + infer_roles(inference, constraint, mode)?; + infer_roles(inference, inner, mode)?; + } + + Type::Forall(binder_id, inner) => { + let binder = inference.context.lookup_forall_binder(binder_id); + + infer_roles(inference, binder.kind, mode.child())?; + infer_roles(inference, inner, mode.child())?; + } + + Type::Function(argument, result) => { + infer_roles(inference, argument, mode.child())?; + infer_roles(inference, result, mode.child())?; + } + + Type::KindApplication(function, argument) => { + infer_roles(inference, function, mode.child())?; + infer_roles(inference, argument, mode.child())?; + } + + Type::Kinded(inner, kind) => { + infer_roles(inference, inner, mode.child())?; + infer_roles(inference, kind, mode.child())?; + } + + Type::OperatorApplication(_, _, left, right) => { + infer_roles(inference, left, mode.child())?; + infer_roles(inference, right, mode.child())?; + } + + Type::Row(row_id) => { + let row = inference.context.lookup_row_type(row_id); + + for field in row.fields.iter() { + infer_roles(inference, field.id, mode.child())?; + } + + if let Some(tail) = row.tail { + infer_roles(inference, tail, mode.child())?; + } + } + + Type::SynonymApplication(synonym_id) => { + let synonym = inference.context.lookup_synonym(synonym_id); + for &argument in synonym.arguments.iter() { + infer_roles(inference, argument, mode.child())?; + } + } + + Type::Constructor(_, _) + | Type::OperatorConstructor(_, _) + | Type::Integer(_) + | Type::String(_, _) + | Type::Unification(_) + | Type::Free(_) + | Type::Unknown(_) => {} + } + + Ok(()) +} diff --git a/tests-integration/fixtures/checking2/001_foreign_check/Main.snap b/tests-integration/fixtures/checking2/001_foreign_check/Main.snap index 7ed97b71..3200412e 100644 --- a/tests-integration/fixtures/checking2/001_foreign_check/Main.snap +++ b/tests-integration/fixtures/checking2/001_foreign_check/Main.snap @@ -9,3 +9,8 @@ Types M :: Type -> Type P :: forall (k :: Type). (k :: Type) -> Type T :: forall (t2 :: Type) (k :: (t2 :: Type)). P @(t2 :: Type) (k :: (t2 :: Type)) -> Type + +Roles +M = [Nominal] +P = [Nominal] +T = [Nominal] diff --git a/tests-integration/fixtures/checking2/003_data_check/Main.snap b/tests-integration/fixtures/checking2/003_data_check/Main.snap index 1df69728..c70f2d44 100644 --- a/tests-integration/fixtures/checking2/003_data_check/Main.snap +++ b/tests-integration/fixtures/checking2/003_data_check/Main.snap @@ -8,3 +8,6 @@ Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: T Types Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/004_data_infer/Main.snap b/tests-integration/fixtures/checking2/004_data_infer/Main.snap index a2931b9d..01b38e4c 100644 --- a/tests-integration/fixtures/checking2/004_data_infer/Main.snap +++ b/tests-integration/fixtures/checking2/004_data_infer/Main.snap @@ -8,3 +8,6 @@ Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 Types Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/005_newtype_check/Main.snap b/tests-integration/fixtures/checking2/005_newtype_check/Main.snap index 4c4f77d8..2f830071 100644 --- a/tests-integration/fixtures/checking2/005_newtype_check/Main.snap +++ b/tests-integration/fixtures/checking2/005_newtype_check/Main.snap @@ -10,3 +10,6 @@ Tagged :: Types Tagged :: forall (k :: Type). (k :: Type) -> Type -> Type + +Roles +Tagged = [Phantom, Representational] diff --git a/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap index 8b44e1d8..390b958c 100644 --- a/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap +++ b/tests-integration/fixtures/checking2/006_newtype_infer/Main.snap @@ -10,3 +10,6 @@ Tagged :: Types Tagged :: forall (t2 :: Type). (t2 :: Type) -> Type -> Type + +Roles +Tagged = [Phantom, Representational] From 44a79e1e56339d11e3d4059d0c73333a564126d0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 02:58:57 +0800 Subject: [PATCH 258/386] Add integration tests for role inference --- .../021_role_inference_phantom/Main.purs | 3 ++ .../021_role_inference_phantom/Main.snap | 13 ++++++++ .../Main.purs | 3 ++ .../Main.snap | 14 +++++++++ .../Main.purs | 3 ++ .../Main.snap | 16 ++++++++++ .../024_role_declaration_strengthen/Main.purs | 5 ++++ .../024_role_declaration_strengthen/Main.snap | 14 +++++++++ .../Main.purs | 5 ++++ .../Main.snap | 30 +++++++++++++++++++ .../026_role_declaration_foreign/Main.purs | 5 ++++ .../026_role_declaration_foreign/Main.snap | 12 ++++++++ .../tests/checking2/generated.rs | 12 ++++++++ 13 files changed, 135 insertions(+) create mode 100644 tests-integration/fixtures/checking2/021_role_inference_phantom/Main.purs create mode 100644 tests-integration/fixtures/checking2/021_role_inference_phantom/Main.snap create mode 100644 tests-integration/fixtures/checking2/022_role_inference_representational/Main.purs create mode 100644 tests-integration/fixtures/checking2/022_role_inference_representational/Main.snap create mode 100644 tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.purs create mode 100644 tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.snap create mode 100644 tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.purs create mode 100644 tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.snap create mode 100644 tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.snap create mode 100644 tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.purs create mode 100644 tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.snap diff --git a/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.purs b/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.purs new file mode 100644 index 00000000..24cc27bb --- /dev/null +++ b/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.purs @@ -0,0 +1,3 @@ +module Main where + +data Proxy a = Proxy diff --git a/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.snap b/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.snap new file mode 100644 index 00000000..01b38e4c --- /dev/null +++ b/tests-integration/fixtures/checking2/021_role_inference_phantom/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 :: Type)) + +Types +Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/022_role_inference_representational/Main.purs b/tests-integration/fixtures/checking2/022_role_inference_representational/Main.purs new file mode 100644 index 00000000..e9dace92 --- /dev/null +++ b/tests-integration/fixtures/checking2/022_role_inference_representational/Main.purs @@ -0,0 +1,3 @@ +module Main where + +data Maybe a = Nothing | Just a diff --git a/tests-integration/fixtures/checking2/022_role_inference_representational/Main.snap b/tests-integration/fixtures/checking2/022_role_inference_representational/Main.snap new file mode 100644 index 00000000..0c24dbcf --- /dev/null +++ b/tests-integration/fixtures/checking2/022_role_inference_representational/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] diff --git a/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.purs b/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.purs new file mode 100644 index 00000000..2e491825 --- /dev/null +++ b/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.purs @@ -0,0 +1,3 @@ +module Main where + +data F f a = F (f a) diff --git a/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.snap b/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.snap new file mode 100644 index 00000000..71ab8526 --- /dev/null +++ b/tests-integration/fixtures/checking2/023_role_inference_nominal_parametric/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +F :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + F @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) + +Types +F :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type + +Roles +F = [Representational, Nominal] diff --git a/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.purs b/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.purs new file mode 100644 index 00000000..500516d1 --- /dev/null +++ b/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.purs @@ -0,0 +1,5 @@ +module Main where + +data Maybe a = Nothing | Just a + +type role Maybe nominal diff --git a/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.snap b/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.snap new file mode 100644 index 00000000..2251a842 --- /dev/null +++ b/tests-integration/fixtures/checking2/024_role_declaration_strengthen/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Nominal] diff --git a/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.purs b/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.purs new file mode 100644 index 00000000..ad53bad9 --- /dev/null +++ b/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.purs @@ -0,0 +1,5 @@ +module Main where + +data F f a = F (f a) + +type role F representational phantom diff --git a/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.snap b/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.snap new file mode 100644 index 00000000..c411ac39 --- /dev/null +++ b/tests-integration/fixtures/checking2/025_role_declaration_loosen_error/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +F :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + F @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) + +Types +F :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type + +Roles +F = [Representational, Nominal] + +Errors +CheckError { + kind: InvalidRoleDeclaration { + index: 1, + declared: Phantom, + inferred: Nominal, + }, + crumbs: [ + TypeDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.purs b/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.purs new file mode 100644 index 00000000..6c59361e --- /dev/null +++ b/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.purs @@ -0,0 +1,5 @@ +module Main where + +foreign import data Effect :: Type -> Type + +type role Effect representational diff --git a/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.snap b/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.snap new file mode 100644 index 00000000..c2f6ffa7 --- /dev/null +++ b/tests-integration/fixtures/checking2/026_role_declaration_foreign/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Effect :: Type -> Type + +Roles +Effect = [Representational] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index f0b61cec..8f785873 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -69,3 +69,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_019_type_operator_chain_precedence_main() { run_test("019_type_operator_chain_precedence", "Main"); } #[rustfmt::skip] #[test] fn test_020_type_operator_chain_kind_error_main() { run_test("020_type_operator_chain_kind_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_021_role_inference_phantom_main() { run_test("021_role_inference_phantom", "Main"); } + +#[rustfmt::skip] #[test] fn test_022_role_inference_representational_main() { run_test("022_role_inference_representational", "Main"); } + +#[rustfmt::skip] #[test] fn test_023_role_inference_nominal_parametric_main() { run_test("023_role_inference_nominal_parametric", "Main"); } + +#[rustfmt::skip] #[test] fn test_024_role_declaration_strengthen_main() { run_test("024_role_declaration_strengthen", "Main"); } + +#[rustfmt::skip] #[test] fn test_025_role_declaration_loosen_error_main() { run_test("025_role_declaration_loosen_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_026_role_declaration_foreign_main() { run_test("026_role_declaration_foreign", "Main"); } From 226b9fdc60fa7adfd77dfa38736bb76451617655 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 16:23:59 +0800 Subject: [PATCH 259/386] Implement initial term signature checking --- compiler-core/checking2/src/lib.rs | 1 + compiler-core/checking2/src/source.rs | 102 +++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 65210c72..0a495254 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -96,6 +96,7 @@ fn check_source(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult< let context = context::CheckContext::new(queries, file_id)?; source::check_type_items(&mut state, &context)?; + source::check_term_items(&mut state, &context)?; Ok(state.checked) } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 8cba731b..b3f82f12 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -89,7 +89,7 @@ where check_type_equation(state, context, &mut scc_state, item)?; } - finalise_binding_group(state, context, &items)?; + finalise_type_binding_group(state, context, &items)?; finalise_roles(state, context, &mut scc_state)?; finalise_data_constructors(state, context, &mut scc_state)?; finalise_synonym_replacements(state, context, &mut scc_state)?; @@ -112,7 +112,7 @@ where } } -fn finalise_binding_group( +fn finalise_type_binding_group( state: &mut CheckState, context: &CheckContext, items: &[TypeItemId], @@ -193,23 +193,23 @@ where match item { TypeItemIr::DataGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; + check_signature_kind(state, context, item_id, *signature)?; } TypeItemIr::NewtypeGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; + check_signature_kind(state, context, item_id, *signature)?; } TypeItemIr::SynonymGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; + check_signature_kind(state, context, item_id, *signature)?; } TypeItemIr::ClassGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; + check_signature_kind(state, context, item_id, *signature)?; } TypeItemIr::Foreign { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; + check_signature_kind(state, context, item_id, *signature)?; } TypeItemIr::Operator { .. } => {} } @@ -217,7 +217,7 @@ where Ok(()) } -fn check_signature_type( +fn check_signature_kind( state: &mut CheckState, context: &CheckContext, item_id: TypeItemId, @@ -226,8 +226,8 @@ fn check_signature_type( where Q: ExternalQueries, { - let (checked_type, _) = types::check_kind(state, context, signature, context.prim.t)?; - state.checked.types.insert(item_id, checked_type); + let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; + state.checked.types.insert(item_id, checked_kind); Ok(()) } @@ -906,3 +906,85 @@ where Ok(arguments.len() == 2) } + +// ------------------------------ Term Items ------------------------------- // + +pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.term_scc { + let items = scc.as_slice(); + + for &item in items { + check_term_signature(state, context, item)?; + } + + finalise_term_binding_group(state, context, items)?; + } + + Ok(()) +} + +fn check_term_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_term_item(item_id) else { + return Ok(()); + }; + + match item { + TermItemIr::Foreign { signature } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; + } + TermItemIr::Operator { .. } => todo!("Operator"), + TermItemIr::ValueGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; + } + _ => (), + } + + Ok(()) +} + +fn check_signature_type( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + signature: lowering::TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; + state.checked.terms.insert(item_id, checked_kind); + Ok(()) +} + +fn finalise_term_binding_group( + state: &mut CheckState, + context: &CheckContext, + items: &[TermItemId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &item_id in items { + let Some(kind) = state.checked.terms.get(&item_id).copied() else { + continue; + }; + + let kind = zonk::zonk(state, context, kind)?; + let kind = generalise::generalise(state, context, kind)?; + state.checked.terms.insert(item_id, kind); + } + + Ok(()) +} From 32ad36fcb5ca9f1951e197ff66fa4579e207c22a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 16:25:32 +0800 Subject: [PATCH 260/386] Add integration tests for foreign and value terms --- .../fixtures/checking2/027_foreign_check/Main.purs | 3 +++ .../fixtures/checking2/027_foreign_check/Main.snap | 9 +++++++++ .../fixtures/checking2/028_value_check/Main.purs | 4 ++++ .../fixtures/checking2/028_value_check/Main.snap | 9 +++++++++ tests-integration/tests/checking2/generated.rs | 4 ++++ 5 files changed, 29 insertions(+) create mode 100644 tests-integration/fixtures/checking2/027_foreign_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/027_foreign_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/028_value_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/028_value_check/Main.snap diff --git a/tests-integration/fixtures/checking2/027_foreign_check/Main.purs b/tests-integration/fixtures/checking2/027_foreign_check/Main.purs new file mode 100644 index 00000000..b1b58614 --- /dev/null +++ b/tests-integration/fixtures/checking2/027_foreign_check/Main.purs @@ -0,0 +1,3 @@ +module Main where + +foreign import unsafeCoerce :: forall a b. a -> b diff --git a/tests-integration/fixtures/checking2/027_foreign_check/Main.snap b/tests-integration/fixtures/checking2/027_foreign_check/Main.snap new file mode 100644 index 00000000..31cbaaf3 --- /dev/null +++ b/tests-integration/fixtures/checking2/027_foreign_check/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/028_value_check/Main.purs b/tests-integration/fixtures/checking2/028_value_check/Main.purs new file mode 100644 index 00000000..f6fc4ba0 --- /dev/null +++ b/tests-integration/fixtures/checking2/028_value_check/Main.purs @@ -0,0 +1,4 @@ +module Main where + +const :: forall a b. a -> b -> a +const a _ = a diff --git a/tests-integration/fixtures/checking2/028_value_check/Main.snap b/tests-integration/fixtures/checking2/028_value_check/Main.snap new file mode 100644 index 00000000..583b2585 --- /dev/null +++ b/tests-integration/fixtures/checking2/028_value_check/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +const :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 8f785873..9f5f8e03 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -81,3 +81,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_025_role_declaration_loosen_error_main() { run_test("025_role_declaration_loosen_error", "Main"); } #[rustfmt::skip] #[test] fn test_026_role_declaration_foreign_main() { run_test("026_role_declaration_foreign", "Main"); } + +#[rustfmt::skip] #[test] fn test_027_foreign_check_main() { run_test("027_foreign_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_028_value_check_main() { run_test("028_value_check", "Main"); } From 34cafe1268b5e8fa48240230a9153a1e34de64f4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 16:45:04 +0800 Subject: [PATCH 261/386] Implement checking for term operators --- compiler-core/checking2/src/core/toolkit.rs | 33 +++++- compiler-core/checking2/src/source.rs | 102 +++++++++++++++--- .../checking2/src/source/operator.rs | 2 +- 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 174bcc6e..babcbcb9 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -74,7 +74,7 @@ where } } -pub fn lookup_file_operator( +pub fn lookup_file_type_operator( state: &CheckState, context: &CheckContext, file_id: FileId, @@ -83,14 +83,41 @@ pub fn lookup_file_operator( where Q: ExternalQueries, { - let kind = if file_id == context.id { + let operator_kind = if file_id == context.id { state.checked.lookup_type(type_id) } else { let checked = context.queries.checked2(file_id)?; checked.lookup_type(type_id) }; - if let Some(kind) = kind { Ok(kind) } else { Ok(context.unknown("invalid operator item")) } + if let Some(operator_kind) = operator_kind { + Ok(operator_kind) + } else { + Ok(context.unknown("invalid operator item")) + } +} + +pub fn lookup_file_term_operator( + state: &CheckState, + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let operator_type = if file_id == context.id { + state.checked.lookup_term(term_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_term(term_id) + }; + + if let Some(operator_type) = operator_type { + Ok(operator_type) + } else { + Ok(context.unknown("invalid operator item")) + } } pub fn inspect_quantified( diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index b3f82f12..79c6d1b2 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -94,7 +94,7 @@ where finalise_data_constructors(state, context, &mut scc_state)?; finalise_synonym_replacements(state, context, &mut scc_state)?; finalise_classes(state, context, &mut scc_state)?; - finalise_operators(state, context, &mut scc_state)?; + finalise_type_operators(state, context, &mut scc_state)?; } Ok(()) } @@ -265,7 +265,7 @@ where scc.foreign.push((item_id, Arc::clone(roles))); } TypeItemIr::Operator { resolution, .. } => { - check_operator_equation(state, context, scc, item_id, *resolution)?; + check_type_operator(state, context, scc, item_id, *resolution)?; } } @@ -841,7 +841,7 @@ where Ok(()) } -fn check_operator_equation( +fn check_type_operator( state: &mut CheckState, context: &CheckContext, scc: &mut TypeSccState, @@ -851,16 +851,13 @@ fn check_operator_equation( where Q: ExternalQueries, { - let Some((file_id, type_id)) = resolution else { - return Ok(()); - }; + let Some((file_id, type_id)) = resolution else { return Ok(()) }; + let operator_kind = toolkit::lookup_file_type_operator(state, context, file_id, type_id)?; - let kind = toolkit::lookup_file_operator(state, context, file_id, type_id)?; - - if let Some(expected) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, kind, expected)?; + if let Some(item_kind) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, operator_kind, item_kind)?; } else { - state.checked.types.insert(item_id, kind); + state.checked.types.insert(item_id, operator_kind); } scc.operator.push(item_id); @@ -868,7 +865,7 @@ where Ok(()) } -fn finalise_operators( +fn finalise_type_operators( state: &mut CheckState, context: &CheckContext, scc: &mut TypeSccState, @@ -909,6 +906,11 @@ where // ------------------------------ Term Items ------------------------------- // +#[derive(Default)] +struct TermSccState { + operator: Vec, +} + pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> where Q: ExternalQueries, @@ -920,7 +922,14 @@ where check_term_signature(state, context, item)?; } + let mut term_scc = TermSccState::default(); + + for &item in items { + check_term_equation(state, context, &mut term_scc, item)?; + } + finalise_term_binding_group(state, context, items)?; + finalise_term_operators(state, context, &mut term_scc)?; } Ok(()) @@ -943,7 +952,6 @@ where let Some(signature) = signature else { return Ok(()) }; check_signature_type(state, context, item_id, *signature)?; } - TermItemIr::Operator { .. } => todo!("Operator"), TermItemIr::ValueGroup { signature, .. } => { let Some(signature) = signature else { return Ok(()) }; check_signature_type(state, context, item_id, *signature)?; @@ -968,6 +976,29 @@ where Ok(()) } +fn check_term_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, + item_id: TermItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_term_item(item_id) else { + return Ok(()); + }; + + match item { + TermItemIr::Operator { resolution, .. } => { + check_term_operator(state, context, scc, item_id, *resolution)?; + } + _ => (), + } + + Ok(()) +} + fn finalise_term_binding_group( state: &mut CheckState, context: &CheckContext, @@ -988,3 +1019,48 @@ where Ok(()) } + +fn check_term_operator( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, + item_id: TermItemId, + resolution: Option<(FileId, TermItemId)>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some((file_id, term_id)) = resolution else { return Ok(()) }; + let operator_type = toolkit::lookup_file_term_operator(state, context, file_id, term_id)?; + + if let Some(item_type) = state.checked.lookup_term(item_id) { + unification::subtype(state, context, operator_type, item_type)?; + } else { + state.checked.terms.insert(item_id, operator_type); + } + + scc.operator.push(item_id); + + Ok(()) +} + +fn finalise_term_operators( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for item_id in mem::take(&mut scc.operator) { + let Some(t) = state.checked.terms.get(&item_id).copied() else { + continue; + }; + if !is_binary_operator_type(state, context, t)? { + let kind_message = state.pretty_id(context, t)?; + state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); + } + } + + Ok(()) +} diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index b89e13da..686cb013 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -303,7 +303,7 @@ impl IsOperator for lowering::TypeId { file_id: FileId, item_id: Self::ItemId, ) -> QueryResult { - toolkit::lookup_file_operator(state, context, file_id, item_id) + toolkit::lookup_file_type_operator(state, context, file_id, item_id) } fn infer_surface( From ee99551679dc61c15e101374d43a9f22605af265 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 16:47:59 +0800 Subject: [PATCH 262/386] Add integration tests for term operators --- .../fixtures/checking2/029_operator_check/Main.purs | 10 ++++++++++ .../fixtures/checking2/029_operator_check/Main.snap | 12 ++++++++++++ tests-integration/tests/checking2/generated.rs | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 tests-integration/fixtures/checking2/029_operator_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/029_operator_check/Main.snap diff --git a/tests-integration/fixtures/checking2/029_operator_check/Main.purs b/tests-integration/fixtures/checking2/029_operator_check/Main.purs new file mode 100644 index 00000000..c54653e0 --- /dev/null +++ b/tests-integration/fixtures/checking2/029_operator_check/Main.purs @@ -0,0 +1,10 @@ +module Main where + +infix 5 const as <: + +const :: forall a b. a -> b -> a +const a _ = a + +infixl 5 add as + + +foreign import add :: Int -> Int -> Int diff --git a/tests-integration/fixtures/checking2/029_operator_check/Main.snap b/tests-integration/fixtures/checking2/029_operator_check/Main.snap new file mode 100644 index 00000000..25e281da --- /dev/null +++ b/tests-integration/fixtures/checking2/029_operator_check/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +<: :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +const :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) ++ :: Int -> Int -> Int +add :: Int -> Int -> Int + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 9f5f8e03..92ace862 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -85,3 +85,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_027_foreign_check_main() { run_test("027_foreign_check", "Main"); } #[rustfmt::skip] #[test] fn test_028_value_check_main() { run_test("028_value_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_029_operator_check_main() { run_test("029_operator_check", "Main"); } From 39687863e4bca94a283891018a319b1ce46cfb21 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 17:15:40 +0800 Subject: [PATCH 263/386] Move item rules to separate modules --- compiler-core/checking2/src/source.rs | 1040 +---------------- .../checking2/src/source/term_items.rs | 170 +++ .../checking2/src/source/type_items.rs | 896 ++++++++++++++ 3 files changed, 1072 insertions(+), 1034 deletions(-) create mode 100644 compiler-core/checking2/src/source/term_items.rs create mode 100644 compiler-core/checking2/src/source/type_items.rs diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 79c6d1b2..10e3b65f 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -7,886 +7,19 @@ pub mod synonym; pub mod terms; pub mod types; -use std::mem; -use std::sync::Arc; +mod term_items; +mod type_items; + +pub use term_items::check_term_items; +pub use type_items::check_type_items; use building_types::QueryResult; -use files::FileId; -use indexing::{TermItemId, TypeItemId}; -use itertools::Itertools; -use lowering::{ - ClassIr, DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, - TypeItemIr, TypeVariableBinding, -}; -use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, generalise, toolkit, - unification, zonk, -}; -use crate::error::{ErrorCrumb, ErrorKind}; +use crate::core::{TypeId, toolkit}; use crate::state::CheckState; -struct PendingDataType { - parameters: Vec, - constructors: Vec<(TermItemId, Vec)>, - declared_roles: Arc<[lowering::Role]>, -} - -struct PendingSynonymType { - kind: TypeId, - parameters: Vec, - synonym: TypeId, -} - -struct PendingClassType { - parameters: Vec, - superclasses: Vec, - functional_dependencies: std::sync::Arc<[lowering::FunctionalDependency]>, - members: Vec<(TermItemId, TypeId)>, -} - -#[derive(Default)] -struct TypeSccState { - data: Vec<(TypeItemId, PendingDataType)>, - synonym: Vec<(TypeItemId, PendingSynonymType)>, - class: Vec<(TypeItemId, PendingClassType)>, - foreign: Vec<(TypeItemId, Arc<[lowering::Role]>)>, - operator: Vec, -} - -/// Checks all type items in topological order. -/// -/// The order is determined by [`GroupedModule::type_scc`] in [`lowering`]. -/// The algorithm accounts for items that appear in [`RecursiveKinds`] by -/// filtering and populating these items with [`Type::Unknown`]. This -/// enables checking of other binding groups, which may be unaffected. -/// -/// [`GroupedModule::type_scc`]: lowering::GroupedModule::type_scc -/// [`RecursiveKinds`]: LoweringError::RecursiveKinds -/// [`Type::Unknown`]: crate::core::Type::Unknown -pub fn check_type_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for scc in &context.grouped.type_scc { - let (items, skipped) = partition_type_items(context, scc); - populate_skipped_items(state, context, &skipped); - - for &item in &items { - check_type_signature(state, context, item)?; - } - - if scc.is_recursive() { - prepare_binding_group(state, context, &items); - } - - let mut scc_state = TypeSccState::default(); - - for &item in &items { - check_type_equation(state, context, &mut scc_state, item)?; - } - - finalise_type_binding_group(state, context, &items)?; - finalise_roles(state, context, &mut scc_state)?; - finalise_data_constructors(state, context, &mut scc_state)?; - finalise_synonym_replacements(state, context, &mut scc_state)?; - finalise_classes(state, context, &mut scc_state)?; - finalise_type_operators(state, context, &mut scc_state)?; - } - Ok(()) -} - -fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TypeItemId]) -where - Q: ExternalQueries, -{ - for &item_id in items { - if state.checked.types.contains_key(&item_id) { - continue; - } - let kind = state.fresh_unification(context.queries, context.prim.t); - state.checked.types.insert(item_id, kind); - } -} - -fn finalise_type_binding_group( - state: &mut CheckState, - context: &CheckContext, - items: &[TypeItemId], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for &item_id in items { - let Some(kind) = state.checked.types.get(&item_id).copied() else { - continue; - }; - - let kind = zonk::zonk(state, context, kind)?; - let kind = generalise::generalise(state, context, kind)?; - state.checked.types.insert(item_id, kind); - } - - Ok(()) -} - -fn partition_type_items( - context: &CheckContext, - scc: &Scc, -) -> (Vec, Vec) -where - Q: ExternalQueries, -{ - let mut checked = vec![]; - let mut skipped = vec![]; - - for &item_id in scc.as_slice() { - if is_recursive_kind(context, item_id) { - skipped.push(item_id); - } else { - checked.push(item_id); - } - } - - (checked, skipped) -} - -fn is_recursive_kind(context: &CheckContext, item_id: TypeItemId) -> bool -where - Q: ExternalQueries, -{ - context.grouped.cycle_errors.iter().any(|error| { - let LoweringError::RecursiveKinds(RecursiveGroup { group }) = error else { - return false; - }; - group.contains(&item_id) - }) -} - -fn populate_skipped_items( - state: &mut CheckState, - context: &CheckContext, - items: &[TypeItemId], -) where - Q: ExternalQueries, -{ - let unknown = context.unknown("invalid recursive type"); - let skipped = items.iter().map(|item| (*item, unknown)); - state.checked.types.extend(skipped); -} - -fn check_type_signature( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some(item) = context.lowered.info.get_type_item(item_id) else { - return Ok(()); - }; - - match item { - TypeItemIr::DataGroup { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_kind(state, context, item_id, *signature)?; - } - TypeItemIr::NewtypeGroup { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_kind(state, context, item_id, *signature)?; - } - TypeItemIr::SynonymGroup { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_kind(state, context, item_id, *signature)?; - } - TypeItemIr::ClassGroup { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_kind(state, context, item_id, *signature)?; - } - TypeItemIr::Foreign { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_kind(state, context, item_id, *signature)?; - } - TypeItemIr::Operator { .. } => {} - } - - Ok(()) -} - -fn check_signature_kind( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, - signature: lowering::TypeId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; - state.checked.types.insert(item_id, checked_kind); - Ok(()) -} - -fn check_type_equation( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, - item_id: TypeItemId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some(item) = context.lowered.info.get_type_item(item_id) else { - return Ok(()); - }; - - match item { - TypeItemIr::DataGroup { signature, data, roles } => { - let Some(DataIr { variables }) = data else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; - } - TypeItemIr::NewtypeGroup { signature, newtype, roles } => { - let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; - check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; - } - TypeItemIr::SynonymGroup { signature, synonym, .. } => { - let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; - check_synonym_equation(state, context, scc, item_id, *signature, variables, *synonym)?; - } - TypeItemIr::ClassGroup { signature, class } => { - let Some(class) = class else { return Ok(()) }; - check_class_equation(state, context, scc, item_id, *signature, class)?; - } - TypeItemIr::Foreign { roles, .. } => { - scc.foreign.push((item_id, Arc::clone(roles))); - } - TypeItemIr::Operator { resolution, .. } => { - check_type_operator(state, context, scc, item_id, *resolution)?; - } - } - - Ok(()) -} - -fn check_data_equation( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, - item_id: TypeItemId, - signature: Option, - variables: &[TypeVariableBinding], - declared_roles: &Arc<[lowering::Role]>, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let parameters = if let Some(signature_id) = signature - && let Some(signature_kind) = state.checked.lookup_type(item_id) - { - check_data_equation_check(state, context, (signature_id, signature_kind), variables)? - } else { - check_data_equation_infer(state, context, item_id, variables)? - }; - - let constructors = check_data_constructors(state, context, item_id)?; - let declared_roles = Arc::clone(declared_roles); - - scc.data.push((item_id, PendingDataType { parameters, constructors, declared_roles })); - - Ok(()) -} - -fn check_data_equation_check( - state: &mut CheckState, - context: &CheckContext, - signature: (lowering::TypeId, TypeId), - bindings: &[TypeVariableBinding], -) -> QueryResult> -where - Q: ExternalQueries, -{ - let signature = signature::inspect_signature(state, context, signature, bindings)?; - check_type_variable_bindings(state, context, bindings, &signature.arguments) -} - -fn check_type_variable_bindings( - state: &mut CheckState, - context: &CheckContext, - bindings: &[TypeVariableBinding], - signature: &[TypeId], -) -> QueryResult> -where - Q: ExternalQueries, -{ - let mut binders = vec![]; - - for (index, equation_binding) in bindings.iter().enumerate() { - let signature_kind = signature.get(index).copied(); - - let kind = resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; - - let name = state.names.fresh(); - state.kind_scope.bind_forall(equation_binding.id, name, kind); - - let text = if let Some(name) = &equation_binding.name { - SmolStr::clone(name) - } else { - name.as_text() - }; - - let text = context.queries.intern_smol_str(text); - let visible = equation_binding.visible; - - binders.push(ForallBinder { visible, name, text, kind }); - } - - Ok(binders) -} - -fn resolve_type_variable_binding( - state: &mut CheckState, - context: &CheckContext, - signature: Option, - binding: &TypeVariableBinding, -) -> QueryResult -where - Q: ExternalQueries, -{ - match (signature, binding.kind) { - (Some(signature_kind), Some(binding_kind)) => { - let (binding_kind, _) = types::infer_kind(state, context, binding_kind)?; - let valid = unification::subtype(state, context, signature_kind, binding_kind)?; - if valid { Ok(binding_kind) } else { Ok(context.unknown("invalid variable kind")) } - } - (Some(signature_kind), None) => { - // Pure checking - Ok(signature_kind) - } - (None, Some(binding_kind)) => { - let (binding_kind, _) = - types::check_kind(state, context, binding_kind, context.prim.t)?; - Ok(binding_kind) - } - (None, None) => { - // Pure inference - Ok(state.fresh_unification(context.queries, context.prim.t)) - } - } -} - -fn check_data_equation_infer( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, - bindings: &[TypeVariableBinding], -) -> QueryResult> -where - Q: ExternalQueries, -{ - let bindings = check_type_variable_bindings(state, context, bindings, &[])?; - let kinds = bindings.iter().map(|binder| binder.kind); - let inferred = context.intern_function_chain(kinds, context.prim.t); - - if let Some(expected) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, inferred, expected)?; - } else { - state.checked.types.insert(item_id, inferred); - } - - Ok(bindings) -} - -fn check_data_constructors( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, -) -> QueryResult)>> -where - Q: ExternalQueries, -{ - let mut constructors = vec![]; - - for constructor_id in context.indexed.pairs.data_constructors(item_id) { - let Some(TermItemIr::Constructor { arguments }) = - context.lowered.info.get_term_item(constructor_id) - else { - continue; - }; - - let mut checked_arguments = vec![]; - for &argument in arguments.iter() { - state.with_error_crumb(ErrorCrumb::ConstructorArgument(argument), |state| { - let (checked_argument, _) = - types::check_kind(state, context, argument, context.prim.t)?; - checked_arguments.push(checked_argument); - Ok(()) - })?; - } - constructors.push((constructor_id, checked_arguments)); - } - - Ok(constructors) -} - -fn finalise_data_constructors( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for (item_id, PendingDataType { parameters, constructors, .. }) in mem::take(&mut scc.data) { - // constructor_kind should have already been generalised by the - // finalise_binding_group function. the kind signature is used - // as the source of truth for constructing the kind applications - let Some(constructor_kind) = state.checked.types.get(&item_id).copied() else { - continue; - }; - - let toolkit::InspectQuantified { binders: kind_binders, quantified } = - toolkit::inspect_quantified(state, context, constructor_kind)?; - - let toolkit::InspectFunction { arguments: parameter_kinds, .. } = - toolkit::inspect_function(state, context, quantified)?; - - // parameter_kinds is the post-generalisation kind for each parameter; - // we want to replace pre-generalisation kinds carried by parameters - // before constructing the signature for the constructor. - let get_parameter_kind = |index: usize| { - if let Some(kind) = parameter_kinds.get(index) { - *kind - } else { - context.unknown("invalid kind") - } - }; - - let type_reference = context.queries.intern_type(Type::Constructor(context.id, item_id)); - - // For the following code loop, let's trace through the declaration: - // - // newtype Tagged :: forall k. k -> Type -> Type - // - for (constructor_id, checked_arguments) in constructors { - let mut result = type_reference; - - // Tagged @k - for binder in &kind_binders { - let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); - result = context.intern_kind_application(result, rigid); - } - - // Tagged @k t a - for (index, parameter) in parameters.iter().enumerate() { - let kind = get_parameter_kind(index); - let rigid = context.intern_rigid(parameter.name, state.depth, kind); - result = context.intern_application(result, rigid); - } - - // a -> Tagged @k t a - for argument in checked_arguments.into_iter().rev() { - let argument = zonk::zonk(state, context, argument)?; - result = context.intern_function(argument, result); - } - - // forall (a :: Type). a -> Tagged @k t a - for (index, parameter) in parameters.iter().enumerate().rev() { - let kind = get_parameter_kind(index); - - let binder = ForallBinder { kind, ..*parameter }; - - let binder_id = context.intern_forall_binder(binder); - result = context.intern_forall(binder_id, result); - } - - // forall (k :: Type) (t :: k) (a :: Type). a -> Tagged @k t a - for binder in kind_binders.iter().rev() { - let binder_id = context.intern_forall_binder(*binder); - result = context.intern_forall(binder_id, result); - } - - state.checked.terms.insert(constructor_id, result); - } - } - - Ok(()) -} - -fn finalise_roles( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for (item_id, pending) in &scc.data { - let PendingDataType { parameters, constructors, declared_roles } = pending; - let inferred_roles = roles::infer_data_roles(state, context, parameters, constructors)?; - let resolved_roles = - roles::check_declared_roles(state, *item_id, &inferred_roles, declared_roles, false); - state.checked.roles.insert(*item_id, resolved_roles); - } - - for (item_id, declared_roles) in mem::take(&mut scc.foreign) { - let Some(kind) = state.checked.lookup_type(item_id) else { - continue; - }; - - let parameter_count = roles::count_kind_arguments(state, context, kind)?; - let inferred_roles = vec![Role::Nominal; parameter_count]; - let resolved_roles = - roles::check_declared_roles(state, item_id, &inferred_roles, &declared_roles, true); - - state.checked.roles.insert(item_id, resolved_roles); - } - - Ok(()) -} - -fn check_synonym_equation( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, - item_id: TypeItemId, - signature: Option, - bindings: &[TypeVariableBinding], - synonym: Option, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let (parameters, kind, result) = if let Some(signature_id) = signature - && let Some(signature_kind) = state.checked.lookup_type(item_id) - { - check_synonym_equation_check(state, context, bindings, (signature_id, signature_kind))? - } else { - check_synonym_equation_infer(state, context, item_id, bindings)? - }; - - let synonym = if let Some(synonym) = synonym { - let (synonym, _) = types::check_kind(state, context, synonym, result)?; - synonym - } else { - context.unknown("invalid synonym type") - }; - - scc.synonym.push((item_id, PendingSynonymType { kind, parameters, synonym })); - - Ok(()) -} - -fn check_synonym_equation_check( - state: &mut CheckState, - context: &CheckContext, - bindings: &[TypeVariableBinding], - (signature_id, signature_kind): (lowering::TypeId, TypeId), -) -> QueryResult<(Vec, TypeId, TypeId)> -where - Q: ExternalQueries, -{ - let signature = - signature::inspect_signature(state, context, (signature_id, signature_kind), bindings)?; - let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; - Ok((parameters, signature_kind, signature.result)) -} - -fn check_synonym_equation_infer( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, - bindings: &[TypeVariableBinding], -) -> QueryResult<(Vec, TypeId, TypeId)> -where - Q: ExternalQueries, -{ - let bindings = check_type_variable_bindings(state, context, bindings, &[])?; - let kinds = bindings.iter().map(|binder| binder.kind); - let result = state.fresh_unification(context.queries, context.prim.t); - let inferred = context.intern_function_chain(kinds, result); - - if let Some(expected) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, inferred, expected)?; - } else { - state.checked.types.insert(item_id, inferred); - } - - Ok((bindings, inferred, result)) -} - -fn finalise_synonym_replacements( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for (item_id, PendingSynonymType { kind, parameters, synonym }) in mem::take(&mut scc.synonym) { - let synonym = zonk::zonk(state, context, synonym)?; - let synonym = CheckedSynonym { kind, parameters, synonym }; - state.checked.synonyms.insert(item_id, synonym); - } - Ok(()) -} - -fn check_class_equation( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, - item_id: TypeItemId, - signature: Option, - class: &ClassIr, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let ClassIr { constraints, variables, functional_dependencies } = class; - - let parameters = if let Some(signature_id) = signature - && let Some(signature_kind) = state.checked.lookup_type(item_id) - { - check_class_equation_check(state, context, variables, (signature_id, signature_kind))? - } else { - check_class_equation_infer(state, context, item_id, variables)? - }; - - let mut superclasses = vec![]; - for &constraint in constraints.iter() { - let (superclass, _) = - types::check_kind(state, context, constraint, context.prim.constraint)?; - superclasses.push(superclass); - } - - let functional_dependencies = Arc::clone(functional_dependencies); - let members = check_class_members(state, context, item_id)?; - - scc.class.push(( - item_id, - PendingClassType { parameters, superclasses, functional_dependencies, members }, - )); - - Ok(()) -} - -fn check_class_equation_check( - state: &mut CheckState, - context: &CheckContext, - bindings: &[TypeVariableBinding], - signature: (lowering::TypeId, TypeId), -) -> QueryResult> -where - Q: ExternalQueries, -{ - let signature = signature::inspect_signature(state, context, signature, bindings)?; - check_type_variable_bindings(state, context, bindings, &signature.arguments) -} - -fn check_class_equation_infer( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, - bindings: &[TypeVariableBinding], -) -> QueryResult> -where - Q: ExternalQueries, -{ - let bindings = check_type_variable_bindings(state, context, bindings, &[])?; - let kinds = bindings.iter().map(|binder| binder.kind); - let inferred = context.intern_function_chain(kinds, context.prim.constraint); - - if let Some(expected) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, inferred, expected)?; - } else { - state.checked.types.insert(item_id, inferred); - } - - Ok(bindings) -} - -fn check_class_members( - state: &mut CheckState, - context: &CheckContext, - item_id: TypeItemId, -) -> QueryResult> -where - Q: ExternalQueries, -{ - let mut members = vec![]; - - for member_id in context.indexed.pairs.class_members(item_id) { - let Some(TermItemIr::ClassMember { signature }) = - context.lowered.info.get_term_item(member_id) - else { - continue; - }; - - let Some(signature_id) = signature else { continue }; - - let (member_type, _) = types::check_kind(state, context, *signature_id, context.prim.t)?; - members.push((member_id, member_type)); - } - - Ok(members) -} - -fn finalise_classes( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for (item_id, pending) in mem::take(&mut scc.class) { - let PendingClassType { parameters, superclasses, functional_dependencies, members } = - pending; - - let Some(class_kind) = state.checked.types.get(&item_id).copied() else { - continue; - }; - - let toolkit::InspectQuantified { binders: class_binders, quantified: class_inner } = - toolkit::inspect_quantified(state, context, class_kind)?; - - let toolkit::InspectFunction { arguments: class_parameters, .. } = - toolkit::inspect_function(state, context, class_inner)?; - - let get_parameter_kind = |index: usize| { - if let Some(kind) = class_parameters.get(index) { - *kind - } else { - context.unknown("invalid kind") - } - }; - - let kind_binders = class_binders - .iter() - .copied() - .map(|binder| context.intern_forall_binder(binder)) - .collect_vec(); - - let type_parameters = parameters - .iter() - .copied() - .enumerate() - .map(|(index, parameter)| { - let kind = get_parameter_kind(index); - let binder = ForallBinder { kind, ..parameter }; - context.intern_forall_binder(binder) - }) - .collect_vec(); - - let mut canonical = context.queries.intern_type(Type::Constructor(context.id, item_id)); - - for binder in &class_binders { - let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); - canonical = context.intern_kind_application(canonical, rigid); - } - - for (index, parameter) in parameters.iter().enumerate() { - let kind = get_parameter_kind(index); - let rigid = context.intern_rigid(parameter.name, state.depth, kind); - canonical = context.intern_application(canonical, rigid); - } - - let superclasses = superclasses - .into_iter() - .map(|superclass| zonk::zonk(state, context, superclass)) - .collect::>>()?; - - for (member_id, member_type) in members.iter() { - let member_type = zonk::zonk(state, context, *member_type)?; - - let toolkit::InspectQuantified { binders: member_binders, quantified: member_inner } = - toolkit::inspect_quantified(state, context, member_type)?; - - let mut result = context.intern_constrained(canonical, member_inner); - - for member_binder in member_binders.iter().copied().rev() { - let binder_id = context.intern_forall_binder(member_binder); - result = context.intern_forall(binder_id, result); - } - - for type_parameter in type_parameters.iter().rev() { - result = context.intern_forall(*type_parameter, result); - } - - for kind_binder in kind_binders.iter().rev() { - result = context.intern_forall(*kind_binder, result); - } - - state.checked.terms.insert(*member_id, result); - } - - let members = members.into_iter().map(|(item_id, _)| item_id).collect_vec(); - - state.checked.classes.insert( - item_id, - CheckedClass { - kind_binders, - type_parameters, - canonical, - superclasses, - functional_dependencies, - members, - }, - ); - } - - Ok(()) -} - -fn check_type_operator( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, - item_id: TypeItemId, - resolution: Option<(FileId, TypeItemId)>, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some((file_id, type_id)) = resolution else { return Ok(()) }; - let operator_kind = toolkit::lookup_file_type_operator(state, context, file_id, type_id)?; - - if let Some(item_kind) = state.checked.lookup_type(item_id) { - unification::subtype(state, context, operator_kind, item_kind)?; - } else { - state.checked.types.insert(item_id, operator_kind); - } - - scc.operator.push(item_id); - - Ok(()) -} - -fn finalise_type_operators( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TypeSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for item_id in mem::take(&mut scc.operator) { - let Some(kind) = state.checked.types.get(&item_id).copied() else { - continue; - }; - - if !is_binary_operator_type(state, context, kind)? { - let kind_message = state.pretty_id(context, kind)?; - state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); - } - } - - Ok(()) -} - fn is_binary_operator_type( state: &mut CheckState, context: &CheckContext, @@ -903,164 +36,3 @@ where Ok(arguments.len() == 2) } - -// ------------------------------ Term Items ------------------------------- // - -#[derive(Default)] -struct TermSccState { - operator: Vec, -} - -pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for scc in &context.grouped.term_scc { - let items = scc.as_slice(); - - for &item in items { - check_term_signature(state, context, item)?; - } - - let mut term_scc = TermSccState::default(); - - for &item in items { - check_term_equation(state, context, &mut term_scc, item)?; - } - - finalise_term_binding_group(state, context, items)?; - finalise_term_operators(state, context, &mut term_scc)?; - } - - Ok(()) -} - -fn check_term_signature( - state: &mut CheckState, - context: &CheckContext, - item_id: TermItemId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some(item) = context.lowered.info.get_term_item(item_id) else { - return Ok(()); - }; - - match item { - TermItemIr::Foreign { signature } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; - } - TermItemIr::ValueGroup { signature, .. } => { - let Some(signature) = signature else { return Ok(()) }; - check_signature_type(state, context, item_id, *signature)?; - } - _ => (), - } - - Ok(()) -} - -fn check_signature_type( - state: &mut CheckState, - context: &CheckContext, - item_id: TermItemId, - signature: lowering::TypeId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; - state.checked.terms.insert(item_id, checked_kind); - Ok(()) -} - -fn check_term_equation( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TermSccState, - item_id: TermItemId, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some(item) = context.lowered.info.get_term_item(item_id) else { - return Ok(()); - }; - - match item { - TermItemIr::Operator { resolution, .. } => { - check_term_operator(state, context, scc, item_id, *resolution)?; - } - _ => (), - } - - Ok(()) -} - -fn finalise_term_binding_group( - state: &mut CheckState, - context: &CheckContext, - items: &[TermItemId], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for &item_id in items { - let Some(kind) = state.checked.terms.get(&item_id).copied() else { - continue; - }; - - let kind = zonk::zonk(state, context, kind)?; - let kind = generalise::generalise(state, context, kind)?; - state.checked.terms.insert(item_id, kind); - } - - Ok(()) -} - -fn check_term_operator( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TermSccState, - item_id: TermItemId, - resolution: Option<(FileId, TermItemId)>, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let Some((file_id, term_id)) = resolution else { return Ok(()) }; - let operator_type = toolkit::lookup_file_term_operator(state, context, file_id, term_id)?; - - if let Some(item_type) = state.checked.lookup_term(item_id) { - unification::subtype(state, context, operator_type, item_type)?; - } else { - state.checked.terms.insert(item_id, operator_type); - } - - scc.operator.push(item_id); - - Ok(()) -} - -fn finalise_term_operators( - state: &mut CheckState, - context: &CheckContext, - scc: &mut TermSccState, -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - for item_id in mem::take(&mut scc.operator) { - let Some(t) = state.checked.terms.get(&item_id).copied() else { - continue; - }; - if !is_binary_operator_type(state, context, t)? { - let kind_message = state.pretty_id(context, t)?; - state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); - } - } - - Ok(()) -} diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs new file mode 100644 index 00000000..8132c2e5 --- /dev/null +++ b/compiler-core/checking2/src/source/term_items.rs @@ -0,0 +1,170 @@ +use std::mem; + +use building_types::QueryResult; +use files::FileId; +use indexing::TermItemId; +use lowering::TermItemIr; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{generalise, toolkit, unification, zonk}; +use crate::error::ErrorKind; +use crate::source::types; +use crate::state::CheckState; + +#[derive(Default)] +struct TermSccState { + operator: Vec, +} + +pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.term_scc { + let items = scc.as_slice(); + + for &item in items { + check_term_signature(state, context, item)?; + } + + let mut term_scc = TermSccState::default(); + + for &item in items { + check_term_equation(state, context, &mut term_scc, item)?; + } + + finalise_term_binding_group(state, context, items)?; + finalise_term_operators(state, context, &mut term_scc)?; + } + + Ok(()) +} + +fn check_term_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_term_item(item_id) else { + return Ok(()); + }; + + match item { + TermItemIr::Foreign { signature } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; + } + TermItemIr::ValueGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_type(state, context, item_id, *signature)?; + } + _ => (), + } + + Ok(()) +} + +fn check_signature_type( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + signature: lowering::TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; + state.checked.terms.insert(item_id, checked_kind); + Ok(()) +} + +fn check_term_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, + item_id: TermItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_term_item(item_id) else { + return Ok(()); + }; + + if let TermItemIr::Operator { resolution, .. } = item { + check_term_operator(state, context, scc, item_id, *resolution)?; + } + + Ok(()) +} + +fn finalise_term_binding_group( + state: &mut CheckState, + context: &CheckContext, + items: &[TermItemId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &item_id in items { + let Some(kind) = state.checked.terms.get(&item_id).copied() else { + continue; + }; + + let kind = zonk::zonk(state, context, kind)?; + let kind = generalise::generalise(state, context, kind)?; + state.checked.terms.insert(item_id, kind); + } + + Ok(()) +} + +fn check_term_operator( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, + item_id: TermItemId, + resolution: Option<(FileId, TermItemId)>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some((file_id, term_id)) = resolution else { return Ok(()) }; + let operator_type = toolkit::lookup_file_term_operator(state, context, file_id, term_id)?; + + if let Some(item_type) = state.checked.lookup_term(item_id) { + unification::subtype(state, context, operator_type, item_type)?; + } else { + state.checked.terms.insert(item_id, operator_type); + } + + scc.operator.push(item_id); + + Ok(()) +} + +fn finalise_term_operators( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TermSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for item_id in mem::take(&mut scc.operator) { + let Some(t) = state.checked.terms.get(&item_id).copied() else { + continue; + }; + + if !super::is_binary_operator_type(state, context, t)? { + let kind_message = state.pretty_id(context, t)?; + state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); + } + } + + Ok(()) +} diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs new file mode 100644 index 00000000..7d50e310 --- /dev/null +++ b/compiler-core/checking2/src/source/type_items.rs @@ -0,0 +1,896 @@ +use std::mem; +use std::sync::Arc; + +use building_types::QueryResult; +use files::FileId; +use indexing::{TermItemId, TypeItemId}; +use itertools::Itertools; +use lowering::{ + ClassIr, DataIr, LoweringError, NewtypeIr, RecursiveGroup, Scc, SynonymIr, TermItemIr, + TypeItemIr, TypeVariableBinding, +}; +use smol_str::SmolStr; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{ + CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, generalise, toolkit, + unification, zonk, +}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::types; +use crate::state::CheckState; + +struct PendingDataType { + parameters: Vec, + constructors: Vec<(TermItemId, Vec)>, + declared_roles: Arc<[lowering::Role]>, +} + +struct PendingSynonymType { + kind: TypeId, + parameters: Vec, + synonym: TypeId, +} + +struct PendingClassType { + parameters: Vec, + superclasses: Vec, + functional_dependencies: Arc<[lowering::FunctionalDependency]>, + members: Vec<(TermItemId, TypeId)>, +} + +#[derive(Default)] +struct TypeSccState { + data: Vec<(TypeItemId, PendingDataType)>, + synonym: Vec<(TypeItemId, PendingSynonymType)>, + class: Vec<(TypeItemId, PendingClassType)>, + foreign: Vec<(TypeItemId, Arc<[lowering::Role]>)>, + operator: Vec, +} + +/// Checks all type items in topological order. +/// +/// The order is determined by [`GroupedModule::type_scc`] in [`lowering`]. +/// The algorithm accounts for items that appear in [`RecursiveKinds`] by +/// filtering and populating these items with [`Type::Unknown`]. This +/// enables checking of other binding groups, which may be unaffected. +/// +/// [`GroupedModule::type_scc`]: lowering::GroupedModule::type_scc +/// [`RecursiveKinds`]: LoweringError::RecursiveKinds +/// [`Type::Unknown`]: crate::core::Type::Unknown +pub fn check_type_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.type_scc { + let (items, skipped) = partition_type_items(context, scc); + populate_skipped_items(state, context, &skipped); + + for &item in &items { + check_type_signature(state, context, item)?; + } + + if scc.is_recursive() { + prepare_binding_group(state, context, &items); + } + + let mut scc_state = TypeSccState::default(); + + for &item in &items { + check_type_equation(state, context, &mut scc_state, item)?; + } + + finalise_type_binding_group(state, context, &items)?; + finalise_roles(state, context, &mut scc_state)?; + finalise_data_constructors(state, context, &mut scc_state)?; + finalise_synonym_replacements(state, context, &mut scc_state)?; + finalise_classes(state, context, &mut scc_state)?; + finalise_type_operators(state, context, &mut scc_state)?; + } + Ok(()) +} + +fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TypeItemId]) +where + Q: ExternalQueries, +{ + for &item_id in items { + if state.checked.types.contains_key(&item_id) { + continue; + } + let kind = state.fresh_unification(context.queries, context.prim.t); + state.checked.types.insert(item_id, kind); + } +} + +fn finalise_type_binding_group( + state: &mut CheckState, + context: &CheckContext, + items: &[TypeItemId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &item_id in items { + let Some(kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let kind = zonk::zonk(state, context, kind)?; + let kind = generalise::generalise(state, context, kind)?; + state.checked.types.insert(item_id, kind); + } + + Ok(()) +} + +fn partition_type_items( + context: &CheckContext, + scc: &Scc, +) -> (Vec, Vec) +where + Q: ExternalQueries, +{ + let mut checked = vec![]; + let mut skipped = vec![]; + + for &item_id in scc.as_slice() { + if is_recursive_kind(context, item_id) { + skipped.push(item_id); + } else { + checked.push(item_id); + } + } + + (checked, skipped) +} + +fn is_recursive_kind(context: &CheckContext, item_id: TypeItemId) -> bool +where + Q: ExternalQueries, +{ + context.grouped.cycle_errors.iter().any(|error| { + let LoweringError::RecursiveKinds(RecursiveGroup { group }) = error else { + return false; + }; + group.contains(&item_id) + }) +} + +fn populate_skipped_items( + state: &mut CheckState, + context: &CheckContext, + items: &[TypeItemId], +) where + Q: ExternalQueries, +{ + let unknown = context.unknown("invalid recursive type"); + let skipped = items.iter().map(|item| (*item, unknown)); + state.checked.types.extend(skipped); +} + +fn check_type_signature( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_type_item(item_id) else { + return Ok(()); + }; + + match item { + TypeItemIr::DataGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_kind(state, context, item_id, *signature)?; + } + TypeItemIr::NewtypeGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_kind(state, context, item_id, *signature)?; + } + TypeItemIr::SynonymGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_kind(state, context, item_id, *signature)?; + } + TypeItemIr::ClassGroup { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_kind(state, context, item_id, *signature)?; + } + TypeItemIr::Foreign { signature, .. } => { + let Some(signature) = signature else { return Ok(()) }; + check_signature_kind(state, context, item_id, *signature)?; + } + TypeItemIr::Operator { .. } => {} + } + + Ok(()) +} + +fn check_signature_kind( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + signature: lowering::TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (checked_kind, _) = types::check_kind(state, context, signature, context.prim.t)?; + state.checked.types.insert(item_id, checked_kind); + Ok(()) +} + +fn check_type_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(item) = context.lowered.info.get_type_item(item_id) else { + return Ok(()); + }; + + match item { + TypeItemIr::DataGroup { signature, data, roles } => { + let Some(DataIr { variables }) = data else { return Ok(()) }; + check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; + } + TypeItemIr::NewtypeGroup { signature, newtype, roles } => { + let Some(NewtypeIr { variables }) = newtype else { return Ok(()) }; + check_data_equation(state, context, scc, item_id, *signature, variables, roles)?; + } + TypeItemIr::SynonymGroup { signature, synonym, .. } => { + let Some(SynonymIr { variables, synonym }) = synonym else { return Ok(()) }; + check_synonym_equation(state, context, scc, item_id, *signature, variables, *synonym)?; + } + TypeItemIr::ClassGroup { signature, class } => { + let Some(class) = class else { return Ok(()) }; + check_class_equation(state, context, scc, item_id, *signature, class)?; + } + TypeItemIr::Foreign { roles, .. } => { + scc.foreign.push((item_id, Arc::clone(roles))); + } + TypeItemIr::Operator { resolution, .. } => { + check_type_operator(state, context, scc, item_id, *resolution)?; + } + } + + Ok(()) +} + +fn check_data_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + signature: Option, + variables: &[TypeVariableBinding], + declared_roles: &Arc<[lowering::Role]>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let parameters = if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + check_data_equation_check(state, context, (signature_id, signature_kind), variables)? + } else { + check_data_equation_infer(state, context, item_id, variables)? + }; + + let constructors = check_data_constructors(state, context, item_id)?; + let declared_roles = Arc::clone(declared_roles); + + scc.data.push((item_id, PendingDataType { parameters, constructors, declared_roles })); + + Ok(()) +} + +fn check_data_equation_check( + state: &mut CheckState, + context: &CheckContext, + signature: (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let signature = super::signature::inspect_signature(state, context, signature, bindings)?; + check_type_variable_bindings(state, context, bindings, &signature.arguments) +} + +fn check_type_variable_bindings( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + signature: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut binders = vec![]; + + for (index, equation_binding) in bindings.iter().enumerate() { + let signature_kind = signature.get(index).copied(); + + let kind = resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; + + let name = state.names.fresh(); + state.kind_scope.bind_forall(equation_binding.id, name, kind); + + let text = if let Some(name) = &equation_binding.name { + SmolStr::clone(name) + } else { + name.as_text() + }; + + let text = context.queries.intern_smol_str(text); + let visible = equation_binding.visible; + + binders.push(ForallBinder { visible, name, text, kind }); + } + + Ok(binders) +} + +fn resolve_type_variable_binding( + state: &mut CheckState, + context: &CheckContext, + signature: Option, + binding: &TypeVariableBinding, +) -> QueryResult +where + Q: ExternalQueries, +{ + match (signature, binding.kind) { + (Some(signature_kind), Some(binding_kind)) => { + let (binding_kind, _) = super::types::infer_kind(state, context, binding_kind)?; + let valid = unification::subtype(state, context, signature_kind, binding_kind)?; + if valid { Ok(binding_kind) } else { Ok(context.unknown("invalid variable kind")) } + } + (Some(signature_kind), None) => { + // Pure checking + Ok(signature_kind) + } + (None, Some(binding_kind)) => { + let (binding_kind, _) = + super::types::check_kind(state, context, binding_kind, context.prim.t)?; + Ok(binding_kind) + } + (None, None) => { + // Pure inference + Ok(state.fresh_unification(context.queries, context.prim.t)) + } + } +} + +fn check_data_equation_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + bindings: &[TypeVariableBinding], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.iter().map(|binder| binder.kind); + let inferred = context.intern_function_chain(kinds, context.prim.t); + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; + } else { + state.checked.types.insert(item_id, inferred); + } + + Ok(bindings) +} + +fn check_data_constructors( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult)>> +where + Q: ExternalQueries, +{ + let mut constructors = vec![]; + + for constructor_id in context.indexed.pairs.data_constructors(item_id) { + let Some(TermItemIr::Constructor { arguments }) = + context.lowered.info.get_term_item(constructor_id) + else { + continue; + }; + + let mut checked_arguments = vec![]; + for &argument in arguments.iter() { + state.with_error_crumb(ErrorCrumb::ConstructorArgument(argument), |state| { + let (checked_argument, _) = + super::types::check_kind(state, context, argument, context.prim.t)?; + checked_arguments.push(checked_argument); + Ok(()) + })?; + } + constructors.push((constructor_id, checked_arguments)); + } + + Ok(constructors) +} + +fn finalise_data_constructors( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, PendingDataType { parameters, constructors, .. }) in mem::take(&mut scc.data) { + // constructor_kind should have already been generalised by the + // finalise_binding_group function. the kind signature is used + // as the source of truth for constructing the kind applications + let Some(constructor_kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let toolkit::InspectQuantified { binders: kind_binders, quantified } = + toolkit::inspect_quantified(state, context, constructor_kind)?; + + let toolkit::InspectFunction { arguments: parameter_kinds, .. } = + toolkit::inspect_function(state, context, quantified)?; + + // parameter_kinds is the post-generalisation kind for each parameter; + // we want to replace pre-generalisation kinds carried by parameters + // before constructing the signature for the constructor. + let get_parameter_kind = |index: usize| { + if let Some(kind) = parameter_kinds.get(index) { + *kind + } else { + context.unknown("invalid kind") + } + }; + + let type_reference = context.queries.intern_type(Type::Constructor(context.id, item_id)); + + // For the following code loop, let's trace through the declaration: + // + // newtype Tagged :: forall k. k -> Type -> Type + // + for (constructor_id, checked_arguments) in constructors { + let mut result = type_reference; + + // Tagged @k + for binder in &kind_binders { + let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); + result = context.intern_kind_application(result, rigid); + } + + // Tagged @k t a + for (index, parameter) in parameters.iter().enumerate() { + let kind = get_parameter_kind(index); + let rigid = context.intern_rigid(parameter.name, state.depth, kind); + result = context.intern_application(result, rigid); + } + + // a -> Tagged @k t a + for argument in checked_arguments.into_iter().rev() { + let argument = zonk::zonk(state, context, argument)?; + result = context.intern_function(argument, result); + } + + // forall (a :: Type). a -> Tagged @k t a + for (index, parameter) in parameters.iter().enumerate().rev() { + let kind = get_parameter_kind(index); + + let binder = ForallBinder { kind, ..*parameter }; + + let binder_id = context.intern_forall_binder(binder); + result = context.intern_forall(binder_id, result); + } + + // forall (k :: Type) (t :: k) (a :: Type). a -> Tagged @k t a + for binder in kind_binders.iter().rev() { + let binder_id = context.intern_forall_binder(*binder); + result = context.intern_forall(binder_id, result); + } + + state.checked.terms.insert(constructor_id, result); + } + } + + Ok(()) +} + +fn finalise_roles( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, pending) in &scc.data { + let PendingDataType { parameters, constructors, declared_roles } = pending; + let inferred_roles = + super::roles::infer_data_roles(state, context, parameters, constructors)?; + let resolved_roles = super::roles::check_declared_roles( + state, + *item_id, + &inferred_roles, + declared_roles, + false, + ); + state.checked.roles.insert(*item_id, resolved_roles); + } + + for (item_id, declared_roles) in mem::take(&mut scc.foreign) { + let Some(kind) = state.checked.lookup_type(item_id) else { + continue; + }; + + let parameter_count = super::roles::count_kind_arguments(state, context, kind)?; + let inferred_roles = vec![Role::Nominal; parameter_count]; + let resolved_roles = super::roles::check_declared_roles( + state, + item_id, + &inferred_roles, + &declared_roles, + true, + ); + + state.checked.roles.insert(item_id, resolved_roles); + } + + Ok(()) +} + +fn check_synonym_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + signature: Option, + bindings: &[TypeVariableBinding], + synonym: Option, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let (parameters, kind, result) = if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + check_synonym_equation_check(state, context, bindings, (signature_id, signature_kind))? + } else { + check_synonym_equation_infer(state, context, item_id, bindings)? + }; + + let synonym = if let Some(synonym) = synonym { + let (synonym, _) = super::types::check_kind(state, context, synonym, result)?; + synonym + } else { + context.unknown("invalid synonym type") + }; + + scc.synonym.push((item_id, PendingSynonymType { kind, parameters, synonym })); + + Ok(()) +} + +fn check_synonym_equation_check( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + (signature_id, signature_kind): (lowering::TypeId, TypeId), +) -> QueryResult<(Vec, TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let signature = super::signature::inspect_signature( + state, + context, + (signature_id, signature_kind), + bindings, + )?; + let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; + Ok((parameters, signature_kind, signature.result)) +} + +fn check_synonym_equation_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + bindings: &[TypeVariableBinding], +) -> QueryResult<(Vec, TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.iter().map(|binder| binder.kind); + let result = state.fresh_unification(context.queries, context.prim.t); + let inferred = context.intern_function_chain(kinds, result); + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; + } else { + state.checked.types.insert(item_id, inferred); + } + + Ok((bindings, inferred, result)) +} + +fn finalise_synonym_replacements( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, PendingSynonymType { kind, parameters, synonym }) in mem::take(&mut scc.synonym) { + let synonym = zonk::zonk(state, context, synonym)?; + let synonym = CheckedSynonym { kind, parameters, synonym }; + state.checked.synonyms.insert(item_id, synonym); + } + Ok(()) +} + +fn check_class_equation( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + signature: Option, + class: &ClassIr, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let ClassIr { constraints, variables, functional_dependencies } = class; + + let parameters = if let Some(signature_id) = signature + && let Some(signature_kind) = state.checked.lookup_type(item_id) + { + check_class_equation_check(state, context, variables, (signature_id, signature_kind))? + } else { + check_class_equation_infer(state, context, item_id, variables)? + }; + + let mut superclasses = vec![]; + for &constraint in constraints.iter() { + let (superclass, _) = + super::types::check_kind(state, context, constraint, context.prim.constraint)?; + superclasses.push(superclass); + } + + let functional_dependencies = Arc::clone(functional_dependencies); + let members = check_class_members(state, context, item_id)?; + + scc.class.push(( + item_id, + PendingClassType { parameters, superclasses, functional_dependencies, members }, + )); + + Ok(()) +} + +fn check_class_equation_check( + state: &mut CheckState, + context: &CheckContext, + bindings: &[TypeVariableBinding], + signature: (lowering::TypeId, TypeId), +) -> QueryResult> +where + Q: ExternalQueries, +{ + let signature = super::signature::inspect_signature(state, context, signature, bindings)?; + check_type_variable_bindings(state, context, bindings, &signature.arguments) +} + +fn check_class_equation_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, + bindings: &[TypeVariableBinding], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let bindings = check_type_variable_bindings(state, context, bindings, &[])?; + let kinds = bindings.iter().map(|binder| binder.kind); + let inferred = context.intern_function_chain(kinds, context.prim.constraint); + + if let Some(expected) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, inferred, expected)?; + } else { + state.checked.types.insert(item_id, inferred); + } + + Ok(bindings) +} + +fn check_class_members( + state: &mut CheckState, + context: &CheckContext, + item_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut members = vec![]; + + for member_id in context.indexed.pairs.class_members(item_id) { + let Some(TermItemIr::ClassMember { signature }) = + context.lowered.info.get_term_item(member_id) + else { + continue; + }; + + let Some(signature_id) = signature else { continue }; + + let (member_type, _) = + super::types::check_kind(state, context, *signature_id, context.prim.t)?; + members.push((member_id, member_type)); + } + + Ok(members) +} + +fn finalise_classes( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (item_id, pending) in mem::take(&mut scc.class) { + let PendingClassType { parameters, superclasses, functional_dependencies, members } = + pending; + + let Some(class_kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + let toolkit::InspectQuantified { binders: class_binders, quantified: class_inner } = + toolkit::inspect_quantified(state, context, class_kind)?; + + let toolkit::InspectFunction { arguments: class_parameters, .. } = + toolkit::inspect_function(state, context, class_inner)?; + + let get_parameter_kind = |index: usize| { + if let Some(kind) = class_parameters.get(index) { + *kind + } else { + context.unknown("invalid kind") + } + }; + + let kind_binders = class_binders + .iter() + .copied() + .map(|binder| context.intern_forall_binder(binder)) + .collect_vec(); + + let type_parameters = parameters + .iter() + .copied() + .enumerate() + .map(|(index, parameter)| { + let kind = get_parameter_kind(index); + let binder = ForallBinder { kind, ..parameter }; + context.intern_forall_binder(binder) + }) + .collect_vec(); + + let mut canonical = context.queries.intern_type(Type::Constructor(context.id, item_id)); + + for binder in &class_binders { + let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); + canonical = context.intern_kind_application(canonical, rigid); + } + + for (index, parameter) in parameters.iter().enumerate() { + let kind = get_parameter_kind(index); + let rigid = context.intern_rigid(parameter.name, state.depth, kind); + canonical = context.intern_application(canonical, rigid); + } + + let superclasses = superclasses + .into_iter() + .map(|superclass| zonk::zonk(state, context, superclass)) + .collect::>>()?; + + for (member_id, member_type) in members.iter() { + let member_type = zonk::zonk(state, context, *member_type)?; + + let toolkit::InspectQuantified { binders: member_binders, quantified: member_inner } = + toolkit::inspect_quantified(state, context, member_type)?; + + let mut result = context.intern_constrained(canonical, member_inner); + + for member_binder in member_binders.iter().copied().rev() { + let binder_id = context.intern_forall_binder(member_binder); + result = context.intern_forall(binder_id, result); + } + + for type_parameter in type_parameters.iter().rev() { + result = context.intern_forall(*type_parameter, result); + } + + for kind_binder in kind_binders.iter().rev() { + result = context.intern_forall(*kind_binder, result); + } + + state.checked.terms.insert(*member_id, result); + } + + let members = members.into_iter().map(|(item_id, _)| item_id).collect_vec(); + + state.checked.classes.insert( + item_id, + CheckedClass { + kind_binders, + type_parameters, + canonical, + superclasses, + functional_dependencies, + members, + }, + ); + } + + Ok(()) +} + +fn check_type_operator( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, + item_id: TypeItemId, + resolution: Option<(FileId, TypeItemId)>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some((file_id, type_id)) = resolution else { return Ok(()) }; + let operator_kind = toolkit::lookup_file_type_operator(state, context, file_id, type_id)?; + + if let Some(item_kind) = state.checked.lookup_type(item_id) { + unification::subtype(state, context, operator_kind, item_kind)?; + } else { + state.checked.types.insert(item_id, operator_kind); + } + + scc.operator.push(item_id); + + Ok(()) +} + +fn finalise_type_operators( + state: &mut CheckState, + context: &CheckContext, + scc: &mut TypeSccState, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for item_id in mem::take(&mut scc.operator) { + let Some(kind) = state.checked.types.get(&item_id).copied() else { + continue; + }; + + if !super::is_binary_operator_type(state, context, kind)? { + let kind_message = state.pretty_id(context, kind)?; + state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); + } + } + + Ok(()) +} From 25ecbbe495f8192ab7956de717882a6dba45adff Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 18:48:45 +0800 Subject: [PATCH 264/386] Implement CheckedNodes structure and accessors --- compiler-core/checking2/src/lib.rs | 62 +++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 0a495254..01f6eb24 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -2,11 +2,11 @@ pub mod context; pub mod core; pub mod error; pub mod implication; -pub mod interners; pub mod safety; pub mod source; pub mod state; +pub mod interners; pub use interners::CoreInterners; use std::sync::Arc; @@ -61,9 +61,29 @@ pub struct CheckedModule { pub synonyms: FxHashMap, pub classes: FxHashMap, pub roles: FxHashMap>, + pub nodes: CheckedNodes, pub errors: Vec, } +#[derive(Debug, Default, PartialEq, Eq)] +pub struct CheckedNodes { + pub types: FxHashMap, + pub expressions: FxHashMap, + pub binders: FxHashMap, + pub lets: FxHashMap, + pub puns: FxHashMap, + pub sections: FxHashMap, + pub term_operator: FxHashMap, + pub type_operator: FxHashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OperatorBranchTypes { + pub left: TypeId, + pub right: TypeId, + pub result: TypeId, +} + impl CheckedModule { pub fn lookup_type(&self, id: TypeItemId) -> Option { self.types.get(&id).copied() @@ -86,6 +106,46 @@ impl CheckedModule { } } +impl CheckedNodes { + pub fn lookup_expression(&self, id: lowering::ExpressionId) -> Option { + self.expressions.get(&id).copied() + } + + pub fn lookup_type(&self, id: lowering::TypeId) -> Option { + self.types.get(&id).copied() + } + + pub fn lookup_binder(&self, id: lowering::BinderId) -> Option { + self.binders.get(&id).copied() + } + + pub fn lookup_let(&self, id: lowering::LetBindingNameGroupId) -> Option { + self.lets.get(&id).copied() + } + + pub fn lookup_pun(&self, id: lowering::RecordPunId) -> Option { + self.puns.get(&id).copied() + } + + pub fn lookup_section(&self, id: lowering::ExpressionId) -> Option { + self.sections.get(&id).copied() + } + + pub fn lookup_type_operator( + &self, + id: lowering::TypeOperatorId, + ) -> Option { + self.type_operator.get(&id).copied() + } + + pub fn lookup_term_operator( + &self, + id: lowering::TermOperatorId, + ) -> Option { + self.term_operator.get(&id).copied() + } +} + pub fn check_module(queries: &impl ExternalQueries, file_id: FileId) -> QueryResult { let prim_id = queries.prim_id(); if file_id == prim_id { check_prim(queries, prim_id) } else { check_source(queries, file_id) } From 7bf9033cdc7913ab4c114c20f91fc240ef1f2333 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 19:03:15 +0800 Subject: [PATCH 265/386] Move instantiate_unifications to toolkit --- compiler-core/checking2/src/core/toolkit.rs | 26 +++++++++++++++++ .../checking2/src/source/operator.rs | 29 ++----------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index babcbcb9..97bc6a53 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -5,6 +5,7 @@ use files::FileId; use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; use crate::core::{CheckedSynonym, ForallBinder, Type, TypeId, normalise}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -183,3 +184,28 @@ where Ok(InspectFunction { arguments, result: current }) } + +pub fn instantiate_unifications( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + + let Type::Forall(binder_id, inner) = context.lookup_type(id) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + id = SubstituteName::one(state, context, binder.name, replacement, inner)?; + } + + Ok(id) +} diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 686cb013..92de102b 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -113,7 +113,7 @@ where OperatorKindMode::Check { expected_type } => (unknown_elaborated, expected_type), }; - let operator_type = instantiate_foralls(state, context, operator_type)?; + let operator_type = toolkit::instantiate_unifications(state, context, operator_type)?; let Some((left_type, operator_type)) = decompose_function_kind(state, context, operator_type)? else { @@ -148,31 +148,6 @@ where E::build(state, context, operator, (left, right), result_type) } -fn instantiate_foralls( - state: &mut CheckState, - context: &CheckContext, - mut id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - safe_loop! { - id = normalise::normalise(state, context, id)?; - - let Type::Forall(binder_id, inner) = context.lookup_type(id) else { - break; - }; - - let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; - - let replacement = state.fresh_unification(context.queries, binder_kind); - id = SubstituteName::one(state, context, binder.name, replacement, inner)?; - } - - Ok(id) -} - fn decompose_function_kind( state: &mut CheckState, context: &CheckContext, @@ -220,7 +195,7 @@ where Q: ExternalQueries, { let operator_kind = toolkit::lookup_file_type(state, context, file_id, type_id)?; - let operator_kind = instantiate_foralls(state, context, operator_kind)?; + let operator_kind = toolkit::instantiate_unifications(state, context, operator_kind)?; let operator_function = toolkit::inspect_function(state, context, operator_kind)?; if operator_function.arguments.len() >= 2 { From e80ed36f33e052a3a43a3c80564393d8bcefe784 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 19:16:52 +0800 Subject: [PATCH 266/386] Move decompose_function_kind to toolkit --- compiler-core/checking2/src/core/toolkit.rs | 91 ++++++++++++++----- .../checking2/src/source/operator.rs | 48 ++-------- 2 files changed, 74 insertions(+), 65 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 97bc6a53..858ea073 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -6,7 +6,7 @@ use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{CheckedSynonym, ForallBinder, Type, TypeId, normalise}; +use crate::core::{CheckedSynonym, ForallBinder, Type, TypeId, normalise, unification}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -146,6 +146,33 @@ where Ok(InspectQuantified { binders, quantified: current }) } +fn try_function( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + + match context.lookup_type(id) { + Type::Function(argument, result) => Ok(Some((argument, result))), + + Type::Application(function_argument, result) => { + let function_argument = normalise::normalise(state, context, function_argument)?; + let Type::Application(function, argument) = context.lookup_type(function_argument) + else { + return Ok(None); + }; + let function = normalise::normalise(state, context, function)?; + if function == context.prim.function { Ok(Some((argument, result))) } else { Ok(None) } + } + + _ => Ok(None), + } +} + pub fn inspect_function( state: &mut CheckState, context: &CheckContext, @@ -158,33 +185,51 @@ where let mut current = id; safe_loop! { - current = normalise::normalise(state, context, current)?; - - match context.lookup_type(current) { - Type::Function(argument, result) => { - arguments.push(argument); - current = result; - } - Type::Application(function_argument, result) => { - let function_argument = normalise::normalise(state, context, function_argument)?; - let Type::Application(function, argument) = context.lookup_type(function_argument) else { - break; - }; - let function = normalise::normalise(state, context, function)?; - if function == context.prim.function { - arguments.push(argument); - current = result; - } else { - break; - } - } - _ => break, - } + let Some((argument, result)) = try_function(state, context, current)? else { + break; + }; + arguments.push(argument); + current = result; } Ok(InspectFunction { arguments, result: current }) } +pub fn decompose_function_kind( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + + match context.lookup_type(id) { + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + + let function_u = context.intern_function(argument_u, result_u); + let _ = unification::solve(state, context, id, unification_id, function_u)?; + + Ok(Some((argument_u, result_u))) + } + + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + let inner = SubstituteName::one(state, context, binder.name, replacement, inner)?; + + decompose_function_kind(state, context, inner) + } + + _ => try_function(state, context, id), + } +} + pub fn instantiate_unifications( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 92de102b..6e0a9f3c 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -7,12 +7,11 @@ use lowering::IsElement; use sugar::OperatorTree; use sugar::bracketing::BracketingResult; +use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::substitute::SubstituteName; -use crate::core::{Type, TypeId, normalise, toolkit, unification}; +use crate::core::{TypeId, normalise, toolkit, unification}; use crate::source::types; use crate::state::CheckState; -use crate::{ExternalQueries, safe_loop}; #[derive(Copy, Clone, Debug)] enum OperatorKindMode { @@ -115,12 +114,14 @@ where let operator_type = toolkit::instantiate_unifications(state, context, operator_type)?; - let Some((left_type, operator_type)) = decompose_function_kind(state, context, operator_type)? + let Some((left_type, operator_type)) = + toolkit::decompose_function_kind(state, context, operator_type)? else { return Ok(unknown); }; - let Some((right_type, result_type)) = decompose_function_kind(state, context, operator_type)? + let Some((right_type, result_type)) = + toolkit::decompose_function_kind(state, context, operator_type)? else { return Ok(unknown); }; @@ -148,43 +149,6 @@ where E::build(state, context, operator, (left, right), result_type) } -fn decompose_function_kind( - state: &mut CheckState, - context: &CheckContext, - id: TypeId, -) -> QueryResult> -where - Q: ExternalQueries, -{ - let id = normalise::normalise(state, context, id)?; - - match context.lookup_type(id) { - Type::Function(argument, result) => Ok(Some((argument, result))), - - Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); - - let function_u = context.intern_function(argument_u, result_u); - let _ = unification::solve(state, context, id, unification_id, function_u)?; - - Ok(Some((argument_u, result_u))) - } - - Type::Forall(binder_id, inner) => { - let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; - - let replacement = state.fresh_unification(context.queries, binder_kind); - let inner = SubstituteName::one(state, context, binder.name, replacement, inner)?; - - decompose_function_kind(state, context, inner) - } - - _ => Ok(None), - } -} - pub fn elaborate_operator_application_kind( state: &mut CheckState, context: &CheckContext, From cc26ea4d9b6357ca3eb773fab5679e9dd49a6433 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 19:33:58 +0800 Subject: [PATCH 267/386] Implement checking rules for binders --- compiler-core/checking2/src/source.rs | 1 + compiler-core/checking2/src/source/binder.rs | 602 +++++++++++++++++++ 2 files changed, 603 insertions(+) create mode 100644 compiler-core/checking2/src/source/binder.rs diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 10e3b65f..6f431841 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -1,5 +1,6 @@ //! Implements syntax-driven checking rules for source files. +pub mod binder; pub mod operator; pub mod roles; pub mod signature; diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs new file mode 100644 index 00000000..2283c188 --- /dev/null +++ b/compiler-core/checking2/src/source/binder.rs @@ -0,0 +1,602 @@ +//! Implements syntax-driven checking rules for binders. + +use std::sync::Arc; + +use building_types::QueryResult; +use itertools::{EitherOrBoth, Itertools}; +use smol_str::SmolStr; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{RowField, RowType, Type, TypeId, normalise, toolkit, unification}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::types; +use crate::state::CheckState; + +#[derive(Copy, Clone, Debug)] +enum BinderMode { + Infer, + Check { expected_type: TypeId, elaborating: bool }, +} + +pub fn infer_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, +) -> QueryResult +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::InferringBinder(binder_id), |state| { + binder_core(state, context, binder_id, BinderMode::Infer) + }) +} + +pub fn check_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + expected_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::CheckingBinder(binder_id), |state| { + binder_core(state, context, binder_id, BinderMode::Check { expected_type, elaborating: true }) + }) +} + +pub fn check_argument_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + expected_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::CheckingBinder(binder_id), |state| { + binder_core( + state, + context, + binder_id, + BinderMode::Check { expected_type, elaborating: false }, + ) + }) +} + +pub fn requires_instantiation(context: &CheckContext, binder_id: lowering::BinderId) -> bool +where + Q: ExternalQueries, +{ + let Some(kind) = context.lowered.info.get_binder_kind(binder_id) else { + return false; + }; + match kind { + lowering::BinderKind::Variable { .. } | lowering::BinderKind::Wildcard => false, + lowering::BinderKind::Named { binder, .. } => { + binder.is_some_and(|id| requires_instantiation(context, id)) + } + lowering::BinderKind::Parenthesized { parenthesized } => { + parenthesized.is_some_and(|id| requires_instantiation(context, id)) + } + lowering::BinderKind::Typed { binder, .. } => { + binder.is_some_and(|id| requires_instantiation(context, id)) + } + _ => true, + } +} + +fn binder_core( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + mode: BinderMode, +) -> QueryResult +where + Q: ExternalQueries, +{ + let unknown = context.unknown("missing binder"); + + let Some(kind) = context.lowered.info.get_binder_kind(binder_id) else { + return Ok(unknown); + }; + + match kind { + lowering::BinderKind::Typed { binder, type_ } => { + let Some(b) = binder else { return Ok(unknown) }; + let Some(t) = type_ else { return Ok(unknown) }; + + let (t, _) = types::infer_kind(state, context, *t)?; + match mode { + BinderMode::Check { elaborating: false, .. } => { + check_argument_binder(state, context, *b, t)?; + } + _ => { + check_binder(state, context, *b, t)?; + } + } + + if let BinderMode::Check { expected_type, elaborating } = mode { + subtype_for_mode(state, context, t, expected_type, elaborating)?; + } + + Ok(t) + } + + lowering::BinderKind::OperatorChain { .. } => { + // TODO(operators): implement IsOperator for BinderId + let inferred_type = context.unknown("operator chain binder"); + + if let BinderMode::Check { expected_type, elaborating } = mode { + subtype_for_mode(state, context, inferred_type, expected_type, elaborating)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Integer { .. } => { + let inferred_type = context.prim.int; + + if let BinderMode::Check { expected_type, .. } = mode { + unification::unify(state, context, inferred_type, expected_type)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Number { .. } => { + let inferred_type = context.prim.number; + + if let BinderMode::Check { expected_type, .. } = mode { + unification::unify(state, context, inferred_type, expected_type)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Constructor { resolution, arguments } => { + let Some((file_id, term_id)) = resolution else { return Ok(unknown) }; + + let mut constructor_t = toolkit::lookup_file_term(state, context, *file_id, *term_id)?; + + let inferred_type = if arguments.is_empty() { + constructor_t = toolkit::instantiate_unifications(state, context, constructor_t)?; + collect_wanteds(state, context, constructor_t)? + } else { + for &argument in arguments.iter() { + constructor_t = check_constructor_binder_application( + state, + context, + constructor_t, + argument, + )?; + } + constructor_t + }; + + if let BinderMode::Check { expected_type, elaborating } = mode { + subtype_for_mode(state, context, inferred_type, expected_type, elaborating)?; + Ok(expected_type) + } else { + Ok(inferred_type) + } + } + + lowering::BinderKind::Variable { .. } => { + let type_id = match mode { + BinderMode::Infer => state.fresh_unification(context.queries, context.prim.t), + BinderMode::Check { expected_type, .. } => expected_type, + }; + state.checked.nodes.binders.insert(binder_id, type_id); + Ok(type_id) + } + + lowering::BinderKind::Named { binder, .. } => { + let Some(binder) = binder else { return Ok(unknown) }; + + let type_id = match mode { + BinderMode::Infer => infer_binder(state, context, *binder)?, + BinderMode::Check { expected_type, elaborating } => { + if elaborating { + check_binder(state, context, *binder, expected_type)? + } else { + check_argument_binder(state, context, *binder, expected_type)? + } + } + }; + state.checked.nodes.binders.insert(binder_id, type_id); + + Ok(type_id) + } + + lowering::BinderKind::Wildcard => match mode { + BinderMode::Infer => Ok(state.fresh_unification(context.queries, context.prim.t)), + BinderMode::Check { expected_type, .. } => Ok(expected_type), + }, + + lowering::BinderKind::String { .. } => { + let inferred_type = context.prim.string; + + if let BinderMode::Check { expected_type, .. } = mode { + unification::unify(state, context, inferred_type, expected_type)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Char { .. } => { + let inferred_type = context.prim.char; + + if let BinderMode::Check { expected_type, .. } = mode { + unification::unify(state, context, inferred_type, expected_type)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Boolean { .. } => { + let inferred_type = context.prim.boolean; + + if let BinderMode::Check { expected_type, .. } = mode { + unification::unify(state, context, inferred_type, expected_type)?; + } + + Ok(inferred_type) + } + + lowering::BinderKind::Array { array } => { + let element_type = state.fresh_unification(context.queries, context.prim.t); + + for binder in array.iter() { + let binder_type = infer_binder(state, context, *binder)?; + unification::subtype_with::( + state, + context, + binder_type, + element_type, + )?; + } + + let array_type = context.intern_application(context.prim.array, element_type); + + if let BinderMode::Check { expected_type, elaborating } = mode { + subtype_for_mode(state, context, array_type, expected_type, elaborating)?; + } + + Ok(array_type) + } + + lowering::BinderKind::Record { record } => { + if let BinderMode::Check { expected_type, elaborating } = mode { + check_record_binder(state, context, binder_id, record, expected_type, elaborating) + } else { + infer_record_binder(state, context, binder_id, record) + } + } + + lowering::BinderKind::Parenthesized { parenthesized } => { + let Some(parenthesized) = parenthesized else { return Ok(unknown) }; + binder_core(state, context, *parenthesized, mode) + } + } +} + +fn subtype_for_mode( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, + elaborating: bool, +) -> QueryResult +where + Q: ExternalQueries, +{ + if elaborating { + unification::subtype(state, context, t1, t2) + } else { + unification::subtype_with::(state, context, t1, t2) + } +} + +/// Collects wanteds from a constrained type, returning the inner type. +fn collect_wanteds( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + loop { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Constrained(constraint, constrained) => { + state.push_wanted(constraint); + id = constrained; + } + _ => return Ok(id), + } + } +} + +/// Applies a constructor type to a binder argument. +pub fn check_function_application_core( + state: &mut CheckState, + context: &CheckContext, + function_t: TypeId, + argument_id: A, + check_argument: F, +) -> QueryResult +where + Q: ExternalQueries, + F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, +{ + let function_t = normalise::normalise(state, context, function_t)?; + + match context.lookup_type(function_t) { + Type::Function(argument_type, result_type) => { + check_argument(state, context, argument_id, argument_type)?; + Ok(result_type) + } + + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + let function_u = context.intern_function(argument_u, result_u); + + unification::solve(state, context, function_t, unification_id, function_u)?; + check_argument(state, context, argument_id, argument_u)?; + + Ok(result_u) + } + + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + let function_t = + SubstituteName::one(state, context, binder.name, replacement, inner)?; + check_function_application_core(state, context, function_t, argument_id, check_argument) + } + + Type::Constrained(constraint, constrained) => { + state.push_wanted(constraint); + check_function_application_core( + state, + context, + constrained, + argument_id, + check_argument, + ) + } + + Type::Application(partial, result_type) => { + let partial = normalise::normalise(state, context, partial)?; + match context.lookup_type(partial) { + Type::Application(constructor, argument_type) => { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.function { + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + if let Type::Unification(unification_id) = context.lookup_type(constructor) { + unification::solve( + state, + context, + constructor, + unification_id, + context.prim.function, + )?; + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + Ok(context.unknown("invalid function application")) + } + _ => Ok(context.unknown("invalid function application")), + } + } + + _ => Ok(context.unknown("invalid function application")), + } +} + +fn check_constructor_binder_application( + state: &mut CheckState, + context: &CheckContext, + constructor_t: TypeId, + binder_id: lowering::BinderId, +) -> QueryResult +where + Q: ExternalQueries, +{ + check_function_application_core(state, context, constructor_t, binder_id, check_binder) +} + +enum PatternItem { + Field(lowering::BinderId), + Pun(lowering::RecordPunId), +} + +fn collect_pattern_items(record: &[lowering::BinderRecordItem]) -> Vec<(SmolStr, PatternItem)> { + let mut items = vec![]; + for field in record { + match field { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + let name = SmolStr::clone(name); + items.push((name, PatternItem::Field(*value))); + } + lowering::BinderRecordItem::RecordPun { id, name } => { + let Some(name) = name else { continue }; + let name = SmolStr::clone(name); + items.push((name, PatternItem::Pun(*id))); + } + } + } + items.sort_by(|a, b| a.0.cmp(&b.0)); + items +} + +fn check_pattern_item( + state: &mut CheckState, + context: &CheckContext, + item: &PatternItem, + expected_type: TypeId, + elaborating: bool, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match *item { + PatternItem::Field(binder_id) => { + if elaborating { + check_binder(state, context, binder_id, expected_type)?; + } else { + check_argument_binder(state, context, binder_id, expected_type)?; + } + } + PatternItem::Pun(pun_id) => { + state.checked.nodes.puns.insert(pun_id, expected_type); + } + } + Ok(()) +} + +fn infer_record_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + record: &[lowering::BinderRecordItem], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut fields = vec![]; + + for field in record.iter() { + match field { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + + let label = SmolStr::clone(name); + let id = infer_binder(state, context, *value)?; + fields.push(RowField { label, id }); + } + lowering::BinderRecordItem::RecordPun { id, name } => { + let Some(name) = name else { continue }; + + let label = SmolStr::clone(name); + let field_type = state.fresh_unification(context.queries, context.prim.t); + + state.checked.nodes.puns.insert(*id, field_type); + fields.push(RowField { label, id: field_type }); + } + } + } + + let row_tail = state.fresh_unification(context.queries, context.prim.row_type); + let row_type = RowType::new(fields, Some(row_tail)); + let row_type_id = context.intern_row_type(row_type); + let row_type = context.intern_row(row_type_id); + let record_type = context.intern_application(context.prim.record, row_type); + + state.checked.nodes.binders.insert(binder_id, record_type); + Ok(record_type) +} + +fn extract_expected_row( + state: &mut CheckState, + context: &CheckContext, + expected_type: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let expected_type = normalise::normalise(state, context, expected_type)?; + let Type::Application(function, argument) = context.lookup_type(expected_type) else { + return Ok(None); + }; + if function != context.prim.record { + return Ok(None); + } + let row = normalise::normalise(state, context, argument)?; + let Type::Row(row_id) = context.lookup_type(row) else { + return Ok(None); + }; + Ok(Some(context.lookup_row_type(row_id))) +} + +fn check_record_binder( + state: &mut CheckState, + context: &CheckContext, + binder_id: lowering::BinderId, + record: &[lowering::BinderRecordItem], + expected_type: TypeId, + elaborating: bool, +) -> QueryResult +where + Q: ExternalQueries, +{ + let pattern_items = collect_pattern_items(record); + + let expected_type = normalise::normalise(state, context, expected_type)?; + + let expected_row = if let Type::Application(function, _) = context.lookup_type(expected_type) + && function == context.prim.record + { + extract_expected_row(state, context, expected_type)? + } else { + None + }; + + let Some(expected_row) = expected_row else { + let result = infer_record_binder(state, context, binder_id, record)?; + unification::unify(state, context, result, expected_type)?; + return Ok(expected_type); + }; + + let mut extra_fields = vec![]; + + let patterns = pattern_items.iter(); + let expected = expected_row.fields.iter(); + + for pair in patterns.merge_join_by(expected, |pattern, expected| pattern.0.cmp(&expected.label)) + { + match pair { + EitherOrBoth::Both((_, item), expected) => { + check_pattern_item(state, context, item, expected.id, elaborating)?; + } + EitherOrBoth::Left((label, item)) => { + let id = state.fresh_unification(context.queries, context.prim.t); + check_pattern_item(state, context, item, id, elaborating)?; + + let label = SmolStr::clone(label); + extra_fields.push(RowField { label, id }); + } + EitherOrBoth::Right(_) => (), + } + } + + if !extra_fields.is_empty() { + if let Some(tail) = expected_row.tail { + let row_tail = state.fresh_unification(context.queries, context.prim.row_type); + + let row_type = RowType::new(extra_fields, Some(row_tail)); + let row_type_id = context.intern_row_type(row_type); + let row_type = context.intern_row(row_type_id); + + unification::unify(state, context, tail, row_type)?; + } else { + let labels = extra_fields.into_iter().map(|field| field.label); + state.insert_error(ErrorKind::AdditionalProperty { labels: Arc::from_iter(labels) }); + } + } + + state.checked.nodes.binders.insert(binder_id, expected_type); + Ok(expected_type) +} From 881496d26e4e4f8566282cdcb41d9bb144016cbf Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 19:36:20 +0800 Subject: [PATCH 268/386] Initial scaffolding for term checking --- .../checking2/src/source/term_items.rs | 101 +++++++- compiler-core/checking2/src/source/terms.rs | 32 +++ .../checking2/src/source/terms/equations.rs | 222 ++++++++++++++++++ 3 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 compiler-core/checking2/src/source/terms/equations.rs diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 8132c2e5..cba98b03 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -7,8 +7,9 @@ use lowering::TermItemIr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{generalise, toolkit, unification, zonk}; -use crate::error::ErrorKind; +use crate::core::{TypeId, generalise, toolkit, unification, zonk}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::terms::equations; use crate::source::types; use crate::state::CheckState; @@ -95,13 +96,105 @@ where return Ok(()); }; - if let TermItemIr::Operator { resolution, .. } = item { - check_term_operator(state, context, scc, item_id, *resolution)?; + match item { + TermItemIr::Operator { resolution, .. } => { + check_term_operator(state, context, scc, item_id, *resolution)?; + } + TermItemIr::ValueGroup { signature, equations } => { + check_value_group(state, context, item_id, *signature, equations)?; + } + _ => (), + } + + Ok(()) +} + +fn check_value_group( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + signature: Option, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::TermDeclaration(item_id), |state| { + check_value_group_core(state, context, item_id, signature, equations) + }) +} + +fn check_value_group_core( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + signature: Option, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + if let Some(signature_id) = signature + && let Some(signature_type) = state.checked.lookup_term(item_id) + { + check_value_group_core_check(state, context, signature_id, signature_type, equations)?; + } else { + check_value_group_core_infer(state, context, item_id, equations)?; } Ok(()) } +fn check_value_group_core_check( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + signature_type: TypeId, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, signature_type)?; + + let toolkit::InspectFunction { arguments, result } = + toolkit::inspect_function(state, context, quantified)?; + + let function = { + let arguments = arguments.iter().copied(); + context.intern_function_chain(arguments, result) + }; + + equations::check_equations_core( + state, + context, + signature_id, + &arguments, + result, + function, + equations, + )?; + + Ok(()) +} + +fn check_value_group_core_infer( + state: &mut CheckState, + context: &CheckContext, + item_id: TermItemId, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let group_type = state.fresh_unification(context.queries, context.prim.t); + state.checked.terms.insert(item_id, group_type); + equations::infer_equations_core(state, context, group_type, equations)?; + + Ok(()) +} + fn finalise_term_binding_group( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/terms.rs b/compiler-core/checking2/src/source/terms.rs index 8b137891..d3799244 100644 --- a/compiler-core/checking2/src/source/terms.rs +++ b/compiler-core/checking2/src/source/terms.rs @@ -1 +1,33 @@ +pub mod equations; +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::TypeId; +use crate::state::CheckState; + +pub fn infer_expression_stub( + state: &mut CheckState, + context: &CheckContext, + _expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO(expressions): implement expression inference + Ok(state.fresh_unification(context.queries, context.prim.t)) +} + +pub fn check_expression_stub( + _state: &mut CheckState, + _context: &CheckContext, + _expression: lowering::ExpressionId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO(expressions): implement expression checking + Ok(expected) +} diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs new file mode 100644 index 00000000..acd61324 --- /dev/null +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -0,0 +1,222 @@ +//! Implements equation checking and inference rules for value groups. + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{TypeId, unification}; +use crate::error::ErrorKind; +use crate::source::binder; +use crate::state::CheckState; + +/// Infers the type of value group equations. +/// +/// For each equation: infer binders, create a result unification variable, +/// build the function type, and subtype against `group_type`. Then infer +/// the guarded expression and subtype against the result type. +pub fn infer_equations_core( + state: &mut CheckState, + context: &CheckContext, + group_type: TypeId, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let minimum_equation_arity = + equations.iter().map(|equation| equation.binders.len()).min().unwrap_or(0); + + for equation in equations { + let mut argument_types = vec![]; + for &binder_id in equation.binders.iter() { + let argument_type = binder::infer_binder(state, context, binder_id)?; + argument_types.push(argument_type); + } + + let result_type = state.fresh_unification(context.queries, context.prim.t); + + let argument_types = &argument_types[..minimum_equation_arity]; + let equation_type = context.intern_function_chain(argument_types.iter().copied(), result_type); + unification::subtype(state, context, equation_type, group_type)?; + + if let Some(guarded) = &equation.guarded { + let inferred_type = infer_guarded_expression(state, context, guarded)?; + unification::subtype(state, context, inferred_type, result_type)?; + } + } + + Ok(()) +} + +/// Checks value group equations against a signature. +/// +/// For each equation: check binders against signature arguments, compute +/// expected result type based on equation arity vs signature arity, then +/// check the guarded expression. +pub fn check_equations_core( + state: &mut CheckState, + context: &CheckContext, + signature_id: lowering::TypeId, + arguments: &[TypeId], + result: TypeId, + function: TypeId, + equations: &[lowering::Equation], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let expected_arity = arguments.len(); + + for equation in equations { + let equation_arity = equation.binders.len(); + + if equation_arity > expected_arity { + state.insert_error(ErrorKind::TooManyBinders { + signature: signature_id, + expected: expected_arity as u32, + actual: equation_arity as u32, + }); + } + + for (&binder_id, &argument_type) in equation.binders.iter().zip(arguments) { + binder::check_argument_binder(state, context, binder_id, argument_type)?; + } + + if equation_arity > expected_arity { + for &binder_id in &equation.binders[expected_arity..] { + binder::infer_binder(state, context, binder_id)?; + } + } + + let expected_type = if equation_arity == 0 { + function + } else if equation_arity >= expected_arity { + result + } else { + let remaining = &arguments[equation_arity..]; + context.intern_function_chain(remaining.iter().copied(), result) + }; + + if let Some(guarded) = &equation.guarded { + check_guarded_expression(state, context, guarded, expected_type)?; + } + } + + Ok(()) +} + +fn infer_guarded_expression( + state: &mut CheckState, + context: &CheckContext, + guarded: &lowering::GuardedExpression, +) -> QueryResult +where + Q: ExternalQueries, +{ + match guarded { + lowering::GuardedExpression::Unconditional { where_expression } => { + let Some(w) = where_expression else { + return Ok(context.unknown("missing guarded expression")); + }; + infer_where_expression(state, context, w) + } + lowering::GuardedExpression::Conditionals { pattern_guarded } => { + let mut inferred_type = context.unknown("empty conditionals"); + for pattern_guarded in pattern_guarded.iter() { + for pattern_guard in pattern_guarded.pattern_guards.iter() { + check_pattern_guard(state, context, pattern_guard)?; + } + if let Some(w) = &pattern_guarded.where_expression { + inferred_type = infer_where_expression(state, context, w)?; + } + } + Ok(inferred_type) + } + } +} + +fn check_guarded_expression( + state: &mut CheckState, + context: &CheckContext, + guarded: &lowering::GuardedExpression, + expected: TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match guarded { + lowering::GuardedExpression::Unconditional { where_expression } => { + let Some(w) = where_expression else { + return Ok(()); + }; + check_where_expression(state, context, w, expected)?; + Ok(()) + } + lowering::GuardedExpression::Conditionals { pattern_guarded } => { + for pattern_guarded in pattern_guarded.iter() { + for pattern_guard in pattern_guarded.pattern_guards.iter() { + check_pattern_guard(state, context, pattern_guard)?; + } + if let Some(w) = &pattern_guarded.where_expression { + check_where_expression(state, context, w, expected)?; + } + } + Ok(()) + } + } +} + +fn check_pattern_guard( + state: &mut CheckState, + context: &CheckContext, + guard: &lowering::PatternGuard, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(expression) = guard.expression else { + return Ok(()); + }; + + // TODO(expressions): implement expression inference + let expression_type = super::infer_expression_stub(state, context, expression)?; + + let Some(binder) = guard.binder else { + return Ok(()); + }; + + binder::check_binder(state, context, binder, expression_type)?; + + Ok(()) +} + +fn infer_where_expression( + state: &mut CheckState, + context: &CheckContext, + where_expression: &lowering::WhereExpression, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO(let-bindings): check_let_chunks for where_expression.bindings + let Some(expression) = where_expression.expression else { + return Ok(context.unknown("missing where expression")); + }; + super::infer_expression_stub(state, context, expression) +} + +fn check_where_expression( + state: &mut CheckState, + context: &CheckContext, + where_expression: &lowering::WhereExpression, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + // TODO(let-bindings): check_let_chunks for where_expression.bindings + let Some(expression) = where_expression.expression else { + return Ok(context.unknown("missing where expression")); + }; + super::check_expression_stub(state, context, expression, expected) +} From 720d292bf91ec32a731824a1d9616ad85277ec47 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 25 Feb 2026 20:04:18 +0800 Subject: [PATCH 269/386] Port over initial rules for term checking --- compiler-core/checking2/src/core/toolkit.rs | 230 ++++++--- .../checking2/src/source/operator.rs | 70 ++- compiler-core/checking2/src/source/terms.rs | 319 ++++++++++++- .../checking2/src/source/terms/application.rs | 187 ++++++++ .../checking2/src/source/terms/collections.rs | 292 ++++++++++++ .../checking2/src/source/terms/equations.rs | 27 +- .../checking2/src/source/terms/form_ado.rs | 276 +++++++++++ .../checking2/src/source/terms/form_do.rs | 439 ++++++++++++++++++ .../checking2/src/source/terms/form_let.rs | 163 +++++++ .../checking2/src/source/terms/forms.rs | 225 +++++++++ 10 files changed, 2144 insertions(+), 84 deletions(-) create mode 100644 compiler-core/checking2/src/source/terms/application.rs create mode 100644 compiler-core/checking2/src/source/terms/collections.rs create mode 100644 compiler-core/checking2/src/source/terms/form_ado.rs create mode 100644 compiler-core/checking2/src/source/terms/form_do.rs create mode 100644 compiler-core/checking2/src/source/terms/form_let.rs create mode 100644 compiler-core/checking2/src/source/terms/forms.rs diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 858ea073..7fcd3e06 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -58,6 +58,36 @@ where if let Some(term) = term { Ok(term) } else { Ok(context.unknown("invalid term item")) } } +pub fn lookup_term_variable( + state: &mut CheckState, + context: &CheckContext, + resolution: lowering::TermVariableResolution, +) -> QueryResult +where + Q: ExternalQueries, +{ + match resolution { + lowering::TermVariableResolution::Binder(binder_id) => Ok(state + .checked + .nodes + .lookup_binder(binder_id) + .unwrap_or_else(|| context.unknown("unresolved binder"))), + lowering::TermVariableResolution::Let(let_binding_id) => Ok(state + .checked + .nodes + .lookup_let(let_binding_id) + .unwrap_or_else(|| context.unknown("unresolved let"))), + lowering::TermVariableResolution::RecordPun(pun_id) => Ok(state + .checked + .nodes + .lookup_pun(pun_id) + .unwrap_or_else(|| context.unknown("unresolved pun"))), + lowering::TermVariableResolution::Reference(file_id, term_id) => { + lookup_file_term(state, context, file_id, term_id) + } + } +} + pub fn lookup_file_synonym( state: &CheckState, context: &CheckContext, @@ -146,91 +176,121 @@ where Ok(InspectQuantified { binders, quantified: current }) } -fn try_function( +pub fn inspect_function( state: &mut CheckState, context: &CheckContext, id: TypeId, -) -> QueryResult> +) -> QueryResult where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; - - match context.lookup_type(id) { - Type::Function(argument, result) => Ok(Some((argument, result))), + let mut arguments = vec![]; + let mut current = id; - Type::Application(function_argument, result) => { - let function_argument = normalise::normalise(state, context, function_argument)?; - let Type::Application(function, argument) = context.lookup_type(function_argument) - else { - return Ok(None); - }; - let function = normalise::normalise(state, context, function)?; - if function == context.prim.function { Ok(Some((argument, result))) } else { Ok(None) } + safe_loop! { + current = normalise::normalise(state, context, current)?; + match context.lookup_type(current) { + Type::Function(argument, result) => { + arguments.push(argument); + current = result; + } + Type::Application(function_argument, result) => { + let function_argument = normalise::normalise(state, context, function_argument)?; + + let Type::Application(function, argument) = context.lookup_type(function_argument) + else { + break; + }; + + let function = normalise::normalise(state, context, function)?; + if function == context.prim.function { + arguments.push(argument); + current = result; + } else { + break; + } + } + _ => break, } - - _ => Ok(None), } + + Ok(InspectFunction { arguments, result: current }) } -pub fn inspect_function( +pub fn instantiate_unifications( state: &mut CheckState, context: &CheckContext, - id: TypeId, -) -> QueryResult + mut id: TypeId, +) -> QueryResult where Q: ExternalQueries, { - let mut arguments = vec![]; - let mut current = id; - safe_loop! { - let Some((argument, result)) = try_function(state, context, current)? else { + id = normalise::normalise(state, context, id)?; + + let Type::Forall(binder_id, inner) = context.lookup_type(id) else { break; }; - arguments.push(argument); - current = result; + + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + id = SubstituteName::one(state, context, binder.name, replacement, inner)?; } - Ok(InspectFunction { arguments, result: current }) + Ok(id) } -pub fn decompose_function_kind( +/// Replaces forall binders with rigid (skolem) variables. +pub fn skolemise_forall( state: &mut CheckState, context: &CheckContext, - id: TypeId, -) -> QueryResult> + mut id: TypeId, +) -> QueryResult where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; - - match context.lookup_type(id) { - Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); + safe_loop! { + id = normalise::normalise(state, context, id)?; - let function_u = context.intern_function(argument_u, result_u); - let _ = unification::solve(state, context, id, unification_id, function_u)?; + let Type::Forall(binder_id, inner) = context.lookup_type(id) else { + break; + }; - Ok(Some((argument_u, result_u))) - } + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; - Type::Forall(binder_id, inner) => { - let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; + let rigid = state.fresh_rigid(context.queries, binder_kind); + id = SubstituteName::one(state, context, binder.name, rigid, inner)?; + } - let replacement = state.fresh_unification(context.queries, binder_kind); - let inner = SubstituteName::one(state, context, binder.name, replacement, inner)?; + Ok(id) +} - decompose_function_kind(state, context, inner) +/// Peels constraint layers, pushing each as a wanted. +pub fn collect_wanteds( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Constrained(constraint, constrained) => { + state.push_wanted(constraint); + id = constrained; + } + _ => return Ok(id), } - - _ => try_function(state, context, id), } } -pub fn instantiate_unifications( +/// Peels constraint layers, pushing each as a given. +pub fn collect_givens( state: &mut CheckState, context: &CheckContext, mut id: TypeId, @@ -240,17 +300,73 @@ where { safe_loop! { id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Constrained(constraint, constrained) => { + state.push_given(constraint); + id = constrained; + } + _ => return Ok(id), + } + } +} - let Type::Forall(binder_id, inner) = context.lookup_type(id) else { - break; - }; +/// Instantiates forall binders and collects wanted constraints. +pub fn instantiate_constrained( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let id = instantiate_unifications(state, context, id)?; + collect_wanteds(state, context, id) +} - let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; +pub fn decompose_function( + state: &mut CheckState, + context: &CheckContext, + t: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let t = normalise::normalise(state, context, t)?; - let replacement = state.fresh_unification(context.queries, binder_kind); - id = SubstituteName::one(state, context, binder.name, replacement, inner)?; - } + match context.lookup_type(t) { + Type::Function(argument, result) => Ok(Some((argument, result))), - Ok(id) + Type::Unification(unification_id) => { + let argument = state.fresh_unification(context.queries, context.prim.t); + let result = state.fresh_unification(context.queries, context.prim.t); + + let function = context.intern_function(argument, result); + unification::solve(state, context, t, unification_id, function)?; + + Ok(Some((argument, result))) + } + + Type::Application(partial, result) => { + let partial = normalise::normalise(state, context, partial)?; + if let Type::Application(constructor, argument) = context.lookup_type(partial) { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.function { + return Ok(Some((argument, result))); + } + if let Type::Unification(unification_id) = context.lookup_type(constructor) { + unification::solve( + state, + context, + constructor, + unification_id, + context.prim.function, + )?; + return Ok(Some((argument, result))); + } + } + Ok(None) + } + + _ => Ok(None), + } } diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 6e0a9f3c..85a4c0d4 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -2,7 +2,7 @@ use building_types::QueryResult; use files::FileId; -use indexing::TypeItemId; +use indexing::{TermItemId, TypeItemId}; use lowering::IsElement; use sugar::OperatorTree; use sugar::bracketing::BracketingResult; @@ -10,7 +10,7 @@ use sugar::bracketing::BracketingResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{TypeId, normalise, toolkit, unification}; -use crate::source::types; +use crate::source::{terms, types}; use crate::state::CheckState; #[derive(Copy, Clone, Debug)] @@ -113,15 +113,15 @@ where }; let operator_type = toolkit::instantiate_unifications(state, context, operator_type)?; - let Some((left_type, operator_type)) = - toolkit::decompose_function_kind(state, context, operator_type)? + toolkit::decompose_function(state, context, operator_type)? else { return Ok(unknown); }; + let operator_type = toolkit::instantiate_unifications(state, context, operator_type)?; let Some((right_type, result_type)) = - toolkit::decompose_function_kind(state, context, operator_type)? + toolkit::decompose_function(state, context, operator_type)? else { return Ok(unknown); }; @@ -214,6 +214,66 @@ pub trait IsOperator: IsElement { ) -> QueryResult<(Self::Elaborated, TypeId)>; } +impl IsOperator for lowering::ExpressionId { + type ItemId = TermItemId; + type Elaborated = (); + + fn unknown_elaborated(_context: &CheckContext) -> Self::Elaborated {} + + fn lookup_tree<'q>( + context: &'q CheckContext, + id: Self, + ) -> Option<&'q BracketingResult> { + context.bracketed.expressions.get(&id) + } + + fn lookup_operator( + context: &CheckContext, + id: Self::OperatorId, + ) -> Option<(FileId, Self::ItemId)> { + context.lowered.info.get_term_operator(id) + } + + fn lookup_item( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: Self::ItemId, + ) -> QueryResult { + toolkit::lookup_file_term_operator(state, context, file_id, item_id) + } + + fn infer_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let inferred_type = terms::infer_expression(state, context, id)?; + Ok(((), inferred_type)) + } + + fn check_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + expected: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let checked_type = terms::check_expression(state, context, id, expected)?; + Ok(((), checked_type)) + } + + fn build( + state: &mut CheckState, + _context: &CheckContext, + (_, _): (FileId, Self::ItemId), + (_, _): (Self::Elaborated, Self::Elaborated), + result_type: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let _ = state; + Ok(((), result_type)) + } +} + impl IsOperator for lowering::TypeId { type ItemId = indexing::TypeItemId; type Elaborated = TypeId; diff --git a/compiler-core/checking2/src/source/terms.rs b/compiler-core/checking2/src/source/terms.rs index d3799244..635fbd72 100644 --- a/compiler-core/checking2/src/source/terms.rs +++ b/compiler-core/checking2/src/source/terms.rs @@ -1,33 +1,330 @@ +pub mod application; +pub mod collections; pub mod equations; +pub mod form_ado; +pub mod form_do; +pub mod form_let; +pub mod forms; use building_types::QueryResult; +use itertools::Itertools; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::TypeId; +use crate::core::{TypeId, normalise, toolkit, unification}; +use crate::error::ErrorCrumb; +use crate::source::{operator, types}; use crate::state::CheckState; -pub fn infer_expression_stub( +/// Checks the type of an expression. +pub fn check_expression( state: &mut CheckState, context: &CheckContext, - _expression: lowering::ExpressionId, + expression: lowering::ExpressionId, + expected: TypeId, ) -> QueryResult where Q: ExternalQueries, { - // TODO(expressions): implement expression inference - Ok(state.fresh_unification(context.queries, context.prim.t)) + state.with_error_crumb(ErrorCrumb::CheckingExpression(expression), |state| { + check_expression_quiet(state, context, expression, expected) + }) } -pub fn check_expression_stub( - _state: &mut CheckState, - _context: &CheckContext, - _expression: lowering::ExpressionId, +fn check_expression_quiet( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expected = normalise::normalise(state, context, expected)?; + let expected = toolkit::skolemise_forall(state, context, expected)?; + let expected = toolkit::collect_givens(state, context, expected)?; + if let Some(section_result) = context.sectioned.expressions.get(&expression) { + check_sectioned_expression(state, context, expression, section_result, expected) + } else { + check_expression_core(state, context, expression, expected) + } +} + +fn check_sectioned_expression( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, + section_result: &sugar::SectionResult, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut current = expected; + let mut parameters = vec![]; + + for §ion_id in section_result.iter() { + let decomposed = toolkit::decompose_function(state, context, current)?; + if let Some((argument_type, result_type)) = decomposed { + state.checked.nodes.sections.insert(section_id, argument_type); + parameters.push(argument_type); + current = result_type; + } else { + let parameter = state.fresh_unification(context.queries, context.prim.t); + state.checked.nodes.sections.insert(section_id, parameter); + parameters.push(parameter); + } + } + + let result_type = infer_expression_core(state, context, expression)?; + let result_type = toolkit::instantiate_constrained(state, context, result_type)?; + + unification::subtype(state, context, result_type, current)?; + + let function_type = context.intern_function_chain(parameters.iter().copied(), result_type); + Ok(function_type) +} + +fn check_expression_core( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, expected: TypeId, ) -> QueryResult where Q: ExternalQueries, { - // TODO(expressions): implement expression checking - Ok(expected) + let unknown = context.unknown("missing expression"); + + let Some(kind) = context.lowered.info.get_expression_kind(expression) else { + return Ok(unknown); + }; + + match kind { + lowering::ExpressionKind::Lambda { binders, expression } => { + forms::check_lambda(state, context, binders, *expression, expected) + } + lowering::ExpressionKind::IfThenElse { if_, then, else_ } => { + forms::check_if_then_else(state, context, *if_, *then, *else_, expected) + } + lowering::ExpressionKind::CaseOf { trunk, branches } => { + forms::check_case_of(state, context, trunk, branches, expected) + } + lowering::ExpressionKind::LetIn { bindings, expression } => { + forms::check_let_in(state, context, bindings, *expression, expected) + } + lowering::ExpressionKind::Parenthesized { parenthesized } => { + let Some(parenthesized) = parenthesized else { return Ok(unknown) }; + check_expression(state, context, *parenthesized, expected) + } + lowering::ExpressionKind::Array { array } => { + collections::check_array(state, context, array, expected) + } + lowering::ExpressionKind::Record { record } => { + collections::check_record(state, context, record, expected) + } + _ => { + let inferred = infer_expression_quiet(state, context, expression)?; + let inferred = toolkit::instantiate_constrained(state, context, inferred)?; + unification::subtype(state, context, inferred, expected)?; + Ok(inferred) + } + } +} + +/// Infers the type of an expression. +pub fn infer_expression( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::InferringExpression(expression), |state| { + infer_expression_quiet(state, context, expression) + }) +} + +fn infer_expression_quiet( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(section_result) = context.sectioned.expressions.get(&expression) { + infer_sectioned_expression(state, context, expression, section_result) + } else { + infer_expression_core(state, context, expression) + } +} + +fn infer_sectioned_expression( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, + section_result: &sugar::SectionResult, +) -> QueryResult +where + Q: ExternalQueries, +{ + let parameter_types = section_result.iter().map(|§ion_id| { + let parameter_type = state.fresh_unification(context.queries, context.prim.t); + state.checked.nodes.sections.insert(section_id, parameter_type); + parameter_type + }); + + let parameter_types = parameter_types.collect_vec(); + + let result_type = infer_expression_core(state, context, expression)?; + let result_type = toolkit::instantiate_constrained(state, context, result_type)?; + + Ok(context.intern_function_chain(parameter_types.iter().copied(), result_type)) +} + +fn infer_expression_core( + state: &mut CheckState, + context: &CheckContext, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let unknown = context.unknown("missing expression"); + + let Some(kind) = context.lowered.info.get_expression_kind(expression) else { + return Ok(unknown); + }; + + match kind { + lowering::ExpressionKind::Typed { expression, type_ } => { + let Some(e) = expression else { return Ok(unknown) }; + let Some(t) = type_ else { return Ok(unknown) }; + + let (t, _) = types::infer_kind(state, context, *t)?; + check_expression(state, context, *e, t)?; + + Ok(t) + } + + lowering::ExpressionKind::OperatorChain { .. } => { + let (_, inferred_type) = operator::infer_operator_chain(state, context, expression)?; + Ok(inferred_type) + } + + lowering::ExpressionKind::InfixChain { head, tail } => { + let Some(head) = *head else { return Ok(unknown) }; + application::infer_infix_chain(state, context, head, tail) + } + + lowering::ExpressionKind::Negate { negate, expression } => { + let Some(negate) = negate else { return Ok(unknown) }; + let Some(expression) = expression else { return Ok(unknown) }; + + let negate_type = toolkit::lookup_term_variable(state, context, *negate)?; + application::check_function_term_application(state, context, negate_type, *expression) + } + + lowering::ExpressionKind::Application { function, arguments } => { + let Some(function) = function else { return Ok(unknown) }; + + let mut function_t = infer_expression(state, context, *function)?; + + for argument in arguments.iter() { + function_t = + application::check_function_application(state, context, function_t, argument)?; + } + + Ok(function_t) + } + + lowering::ExpressionKind::IfThenElse { if_, then, else_ } => { + forms::infer_if_then_else(state, context, *if_, *then, *else_) + } + + lowering::ExpressionKind::LetIn { bindings, expression } => { + form_let::check_let_chunks(state, context, bindings)?; + + let Some(expression) = expression else { return Ok(unknown) }; + + infer_expression(state, context, *expression) + } + + lowering::ExpressionKind::Lambda { binders, expression } => { + forms::infer_lambda(state, context, binders, *expression) + } + + lowering::ExpressionKind::CaseOf { trunk, branches } => { + forms::infer_case_of(state, context, trunk, branches) + } + + lowering::ExpressionKind::Do { bind, discard, statements } => { + form_do::infer_do(state, context, *bind, *discard, statements) + } + + lowering::ExpressionKind::Ado { map, apply, pure, statements, expression } => { + form_ado::infer_ado(state, context, *map, *apply, *pure, statements, *expression) + } + + lowering::ExpressionKind::Constructor { resolution } => { + let Some((file_id, term_id)) = resolution else { return Ok(unknown) }; + toolkit::lookup_file_term(state, context, *file_id, *term_id) + } + + lowering::ExpressionKind::Variable { resolution } => { + let Some(resolution) = *resolution else { return Ok(unknown) }; + toolkit::lookup_term_variable(state, context, resolution) + } + + lowering::ExpressionKind::OperatorName { resolution } => { + let Some((file_id, term_id)) = resolution else { return Ok(unknown) }; + toolkit::lookup_file_term(state, context, *file_id, *term_id) + } + + lowering::ExpressionKind::Section => { + if let Some(type_id) = state.checked.nodes.lookup_section(expression) { + Ok(type_id) + } else { + Ok(unknown) + } + } + + lowering::ExpressionKind::Hole => Ok(unknown), + + lowering::ExpressionKind::String => Ok(context.prim.string), + + lowering::ExpressionKind::Char => Ok(context.prim.char), + + lowering::ExpressionKind::Boolean { .. } => Ok(context.prim.boolean), + + lowering::ExpressionKind::Integer => Ok(context.prim.int), + + lowering::ExpressionKind::Number => Ok(context.prim.number), + + lowering::ExpressionKind::Array { array } => { + collections::infer_array(state, context, array) + } + + lowering::ExpressionKind::Record { record } => { + collections::infer_record(state, context, record) + } + + lowering::ExpressionKind::Parenthesized { parenthesized } => { + let Some(parenthesized) = parenthesized else { return Ok(unknown) }; + infer_expression(state, context, *parenthesized) + } + + lowering::ExpressionKind::RecordAccess { record, labels } => { + let Some(record) = *record else { return Ok(unknown) }; + let Some(labels) = labels else { return Ok(unknown) }; + collections::infer_record_access(state, context, record, labels) + } + + lowering::ExpressionKind::RecordUpdate { record, updates } => { + let Some(record) = *record else { return Ok(unknown) }; + collections::infer_record_update(state, context, record, updates) + } + } } diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs new file mode 100644 index 00000000..29f2b079 --- /dev/null +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -0,0 +1,187 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, unification}; +use crate::source::types; +use crate::state::CheckState; + +pub fn infer_infix_chain( + state: &mut CheckState, + context: &CheckContext, + head: lowering::ExpressionId, + tail: &[lowering::InfixPair], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut infix_type = super::infer_expression(state, context, head)?; + + for lowering::InfixPair { tick, element } in tail.iter() { + let Some(tick) = tick else { return Ok(context.unknown("missing infix tick")) }; + let Some(element) = element else { return Ok(context.unknown("missing infix element")) }; + + let tick_type = super::infer_expression(state, context, *tick)?; + let applied_tick = check_function_application_core( + state, + context, + tick_type, + infix_type, + |state, context, infix_type, expected_type| { + unification::subtype(state, context, infix_type, expected_type)?; + Ok(infix_type) + }, + )?; + + infix_type = check_function_term_application(state, context, applied_tick, *element)?; + } + + Ok(infix_type) +} + +/// Generic function for application checking. +pub fn check_function_application_core( + state: &mut CheckState, + context: &CheckContext, + function_t: TypeId, + argument_id: A, + check_argument: F, +) -> QueryResult +where + Q: ExternalQueries, + F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, +{ + let function_t = normalise::normalise(state, context, function_t)?; + + match context.lookup_type(function_t) { + Type::Function(argument_type, result_type) => { + check_argument(state, context, argument_id, argument_type)?; + Ok(result_type) + } + + Type::Unification(unification_id) => { + let argument_u = state.fresh_unification(context.queries, context.prim.t); + let result_u = state.fresh_unification(context.queries, context.prim.t); + let function_u = context.intern_function(argument_u, result_u); + + unification::solve(state, context, function_t, unification_id, function_u)?; + check_argument(state, context, argument_id, argument_u)?; + + Ok(result_u) + } + + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let replacement = state.fresh_unification(context.queries, binder_kind); + let function_t = SubstituteName::one(state, context, binder.name, replacement, inner)?; + check_function_application_core(state, context, function_t, argument_id, check_argument) + } + + Type::Constrained(constraint, constrained) => { + state.push_wanted(constraint); + check_function_application_core( + state, + context, + constrained, + argument_id, + check_argument, + ) + } + + Type::Application(partial, result_type) => { + let partial = normalise::normalise(state, context, partial)?; + match context.lookup_type(partial) { + Type::Application(constructor, argument_type) => { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.function { + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + if let Type::Unification(unification_id) = context.lookup_type(constructor) { + unification::solve( + state, + context, + constructor, + unification_id, + context.prim.function, + )?; + check_argument(state, context, argument_id, argument_type)?; + return Ok(result_type); + } + Ok(context.unknown("invalid function application")) + } + _ => Ok(context.unknown("invalid function application")), + } + } + + _ => Ok(context.unknown("invalid function application")), + } +} + +pub fn check_function_term_application( + state: &mut CheckState, + context: &CheckContext, + function_t: TypeId, + expression_id: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + check_function_application_core( + state, + context, + function_t, + expression_id, + super::check_expression, + ) +} + +pub fn check_function_application( + state: &mut CheckState, + context: &CheckContext, + function_t: TypeId, + argument: &lowering::ExpressionArgument, +) -> QueryResult +where + Q: ExternalQueries, +{ + match argument { + lowering::ExpressionArgument::Type(type_argument) => { + let Some(type_argument) = type_argument else { + return Ok(context.unknown("missing type argument")); + }; + check_function_type_application(state, context, function_t, *type_argument) + } + lowering::ExpressionArgument::Term(term_argument) => { + let Some(term_argument) = term_argument else { + return Ok(context.unknown("missing term argument")); + }; + check_function_term_application(state, context, function_t, *term_argument) + } + } +} + +pub fn check_function_type_application( + state: &mut CheckState, + context: &CheckContext, + function_t: TypeId, + argument: lowering::TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let function_t = normalise::normalise(state, context, function_t)?; + match context.lookup_type(function_t) { + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + + let (argument_type, _) = types::check_kind(state, context, argument, binder_kind)?; + SubstituteName::one(state, context, binder.name, argument_type, inner) + } + _ => Ok(context.unknown("invalid type application")), + } +} diff --git a/compiler-core/checking2/src/source/terms/collections.rs b/compiler-core/checking2/src/source/terms/collections.rs new file mode 100644 index 00000000..ff1f8c78 --- /dev/null +++ b/compiler-core/checking2/src/source/terms/collections.rs @@ -0,0 +1,292 @@ +use building_types::QueryResult; +use smol_str::SmolStr; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{RowField, RowType, Type, TypeId, normalise, toolkit, unification}; +use crate::state::CheckState; + +pub fn infer_array( + state: &mut CheckState, + context: &CheckContext, + array: &[lowering::ExpressionId], +) -> QueryResult +where + Q: ExternalQueries, +{ + let inferred_type = state.fresh_unification(context.queries, context.prim.t); + + for expression in array.iter() { + let element_type = super::infer_expression(state, context, *expression)?; + unification::subtype(state, context, element_type, inferred_type)?; + } + + let array_type = context.intern_application(context.prim.array, inferred_type); + + Ok(array_type) +} + +pub fn check_array( + state: &mut CheckState, + context: &CheckContext, + array: &[lowering::ExpressionId], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let normalised = normalise::normalise(state, context, expected)?; + if let Type::Application(constructor, element_type) = context.lookup_type(normalised) { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.array { + for expression in array.iter() { + super::check_expression(state, context, *expression, element_type)?; + } + return Ok(expected); + } + } + + let inferred = infer_array(state, context, array)?; + unification::subtype(state, context, inferred, expected)?; + Ok(inferred) +} + +pub fn infer_record( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::ExpressionRecordItem], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut fields = vec![]; + + for field in record.iter() { + match field { + lowering::ExpressionRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + + let label = SmolStr::clone(name); + let id = super::infer_expression(state, context, *value)?; + let id = toolkit::instantiate_unifications(state, context, id)?; + let id = toolkit::collect_wanteds(state, context, id)?; + + fields.push(RowField { label, id }); + } + lowering::ExpressionRecordItem::RecordPun { name, resolution } => { + let Some(name) = name else { continue }; + let Some(resolution) = resolution else { continue }; + + let label = SmolStr::clone(name); + let id = toolkit::lookup_term_variable(state, context, *resolution)?; + let id = toolkit::instantiate_unifications(state, context, id)?; + let id = toolkit::collect_wanteds(state, context, id)?; + + fields.push(RowField { label, id }); + } + } + } + + let row_type = RowType::new(fields, None); + let row_type_id = context.intern_row_type(row_type); + let row_type = context.intern_row(row_type_id); + let record_type = context.intern_application(context.prim.record, row_type); + + Ok(record_type) +} + +pub fn check_record( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::ExpressionRecordItem], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let normalised = normalise::normalise(state, context, expected)?; + if let Type::Application(constructor, row_type) = context.lookup_type(normalised) { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.record { + let row_type = normalise::normalise(state, context, row_type)?; + if let Type::Row(row_id) = context.lookup_type(row_type) { + let expected_fields = context.lookup_row_type(row_id); + + let mut fields = vec![]; + + for field in record.iter() { + match field { + lowering::ExpressionRecordItem::RecordField { name, value } => { + let Some(name) = name else { continue }; + let Some(value) = value else { continue }; + + let label = SmolStr::clone(name); + + let expected_field_type = expected_fields + .fields + .iter() + .find(|f| f.label == label) + .map(|f| f.id); + + let id = if let Some(expected_type) = expected_field_type { + super::check_expression(state, context, *value, expected_type)? + } else { + let id = super::infer_expression(state, context, *value)?; + let id = toolkit::instantiate_unifications(state, context, id)?; + toolkit::collect_wanteds(state, context, id)? + }; + + fields.push(RowField { label, id }); + } + lowering::ExpressionRecordItem::RecordPun { name, resolution } => { + let Some(name) = name else { continue }; + let Some(resolution) = resolution else { continue }; + + let label = SmolStr::clone(name); + + let expected_field_type = expected_fields + .fields + .iter() + .find(|f| f.label == label) + .map(|f| f.id); + + let id = toolkit::lookup_term_variable(state, context, *resolution)?; + + let id = if let Some(expected_type) = expected_field_type { + unification::subtype(state, context, id, expected_type)?; + id + } else { + let id = toolkit::instantiate_unifications(state, context, id)?; + toolkit::collect_wanteds(state, context, id)? + }; + + fields.push(RowField { label, id }); + } + } + } + + let row_type = RowType::new(fields, None); + let row_type_id = context.intern_row_type(row_type); + let row_type = context.intern_row(row_type_id); + let record_type = context.intern_application(context.prim.record, row_type); + + unification::subtype(state, context, record_type, expected)?; + return Ok(record_type); + } + } + } + + let inferred = infer_record(state, context, record)?; + unification::subtype(state, context, inferred, expected)?; + Ok(inferred) +} + +pub fn infer_record_access( + state: &mut CheckState, + context: &CheckContext, + record: lowering::ExpressionId, + labels: &[SmolStr], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut current_type = super::infer_expression(state, context, record)?; + + for label in labels.iter() { + let label = SmolStr::clone(label); + + let field_type = state.fresh_unification(context.queries, context.prim.t); + let tail_type = state.fresh_unification(context.queries, context.prim.row_type); + + let row_type = RowType::new(vec![RowField { label, id: field_type }], Some(tail_type)); + let row_type_id = context.intern_row_type(row_type); + let row_type = context.intern_row(row_type_id); + let record_type = context.intern_application(context.prim.record, row_type); + + unification::subtype(state, context, current_type, record_type)?; + current_type = field_type; + } + + Ok(current_type) +} + +pub fn infer_record_update( + state: &mut CheckState, + context: &CheckContext, + record: lowering::ExpressionId, + updates: &[lowering::RecordUpdate], +) -> QueryResult +where + Q: ExternalQueries, +{ + let (input_fields, output_fields, tail) = infer_record_updates(state, context, updates)?; + + let input_row = RowType::new(input_fields, Some(tail)); + let input_row_id = context.intern_row_type(input_row); + let input_row = context.intern_row(input_row_id); + let input_record = context.intern_application(context.prim.record, input_row); + + let output_row = RowType::new(output_fields, Some(tail)); + let output_row_id = context.intern_row_type(output_row); + let output_row = context.intern_row(output_row_id); + let output_record = context.intern_application(context.prim.record, output_row); + + super::check_expression(state, context, record, input_record)?; + + Ok(output_record) +} + +pub fn infer_record_updates( + state: &mut CheckState, + context: &CheckContext, + updates: &[lowering::RecordUpdate], +) -> QueryResult<(Vec, Vec, TypeId)> +where + Q: ExternalQueries, +{ + let mut input_fields = vec![]; + let mut output_fields = vec![]; + + for update in updates { + match update { + lowering::RecordUpdate::Leaf { name, expression } => { + let Some(name) = name else { continue }; + let label = SmolStr::clone(name); + + let input_id = state.fresh_unification(context.queries, context.prim.t); + let output_id = if let Some(expression) = expression { + super::infer_expression(state, context, *expression)? + } else { + context.unknown("missing record update expression") + }; + + input_fields.push(RowField { label: label.clone(), id: input_id }); + output_fields.push(RowField { label, id: output_id }); + } + lowering::RecordUpdate::Branch { name, updates } => { + let Some(name) = name else { continue }; + let label = SmolStr::clone(name); + + let (in_f, out_f, tail) = infer_record_updates(state, context, updates)?; + + let in_row = RowType::new(in_f, Some(tail)); + let in_row_id = context.intern_row_type(in_row); + let in_row = context.intern_row(in_row_id); + let in_id = context.intern_application(context.prim.record, in_row); + + let out_row = RowType::new(out_f, Some(tail)); + let out_row_id = context.intern_row_type(out_row); + let out_row = context.intern_row(out_row_id); + let out_id = context.intern_application(context.prim.record, out_row); + + input_fields.push(RowField { label: label.clone(), id: in_id }); + output_fields.push(RowField { label, id: out_id }); + } + } + } + + let tail = state.fresh_unification(context.queries, context.prim.row_type); + + Ok((input_fields, output_fields, tail)) +} diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index acd61324..faf6f077 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -6,7 +6,8 @@ use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{TypeId, unification}; use crate::error::ErrorKind; -use crate::source::binder; +use crate::source::terms::form_let; +use crate::source::{binder, terms}; use crate::state::CheckState; /// Infers the type of value group equations. @@ -36,7 +37,8 @@ where let result_type = state.fresh_unification(context.queries, context.prim.t); let argument_types = &argument_types[..minimum_equation_arity]; - let equation_type = context.intern_function_chain(argument_types.iter().copied(), result_type); + let equation_type = + context.intern_function_chain(argument_types.iter().copied(), result_type); unification::subtype(state, context, equation_type, group_type)?; if let Some(guarded) = &equation.guarded { @@ -105,7 +107,7 @@ where Ok(()) } -fn infer_guarded_expression( +pub fn infer_guarded_expression( state: &mut CheckState, context: &CheckContext, guarded: &lowering::GuardedExpression, @@ -135,7 +137,7 @@ where } } -fn check_guarded_expression( +pub fn check_guarded_expression( state: &mut CheckState, context: &CheckContext, guarded: &lowering::GuardedExpression, @@ -178,8 +180,7 @@ where return Ok(()); }; - // TODO(expressions): implement expression inference - let expression_type = super::infer_expression_stub(state, context, expression)?; + let expression_type = super::infer_expression(state, context, expression)?; let Some(binder) = guard.binder else { return Ok(()); @@ -190,7 +191,7 @@ where Ok(()) } -fn infer_where_expression( +pub fn infer_where_expression( state: &mut CheckState, context: &CheckContext, where_expression: &lowering::WhereExpression, @@ -198,11 +199,13 @@ fn infer_where_expression( where Q: ExternalQueries, { - // TODO(let-bindings): check_let_chunks for where_expression.bindings + form_let::check_let_chunks(state, context, &where_expression.bindings)?; + let Some(expression) = where_expression.expression else { return Ok(context.unknown("missing where expression")); }; - super::infer_expression_stub(state, context, expression) + + terms::infer_expression(state, context, expression) } fn check_where_expression( @@ -214,9 +217,11 @@ fn check_where_expression( where Q: ExternalQueries, { - // TODO(let-bindings): check_let_chunks for where_expression.bindings + form_let::check_let_chunks(state, context, &where_expression.bindings)?; + let Some(expression) = where_expression.expression else { return Ok(context.unknown("missing where expression")); }; - super::check_expression_stub(state, context, expression, expected) + + terms::check_expression(state, context, expression, expected) } diff --git a/compiler-core/checking2/src/source/terms/form_ado.rs b/compiler-core/checking2/src/source/terms/form_ado.rs new file mode 100644 index 00000000..65c5be4e --- /dev/null +++ b/compiler-core/checking2/src/source/terms/form_ado.rs @@ -0,0 +1,276 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{TypeId, unification}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::binder; +use crate::source::terms::{application, form_do, form_let}; +use crate::state::CheckState; + +enum AdoStep<'a> { + Action { + statement: lowering::DoStatementId, + binder_type: TypeId, + expression: lowering::ExpressionId, + }, + Let { + statement: lowering::DoStatementId, + statements: &'a [lowering::LetBindingChunk], + }, +} + +pub fn infer_ado( + state: &mut CheckState, + context: &CheckContext, + map: Option, + apply: Option, + pure: Option, + statement_ids: &[lowering::DoStatementId], + expression: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + // First, perform a forward pass where variable bindings are bound + // to unification variables. Let bindings are not checked here to + // avoid premature solving of unification variables. Instead, they + // are checked inline during the statement checking loop. + let mut steps = vec![]; + for &statement_id in statement_ids.iter() { + let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { + continue; + }; + match statement { + lowering::DoStatement::Bind { binder, expression } => { + let binder_type = if let Some(binder) = binder { + binder::infer_binder(state, context, *binder)? + } else { + state.fresh_unification(context.queries, context.prim.t) + }; + let Some(expression) = *expression else { continue }; + steps.push(AdoStep::Action { statement: statement_id, binder_type, expression }); + } + lowering::DoStatement::Let { statements } => { + steps.push(AdoStep::Let { statement: statement_id, statements }); + } + lowering::DoStatement::Discard { expression } => { + let binder_type = state.fresh_unification(context.queries, context.prim.t); + let Some(expression) = *expression else { continue }; + steps.push(AdoStep::Action { statement: statement_id, binder_type, expression }); + } + } + } + + let binder_types: Vec<_> = steps + .iter() + .filter_map(|step| match step { + AdoStep::Action { binder_type, .. } => Some(*binder_type), + AdoStep::Let { .. } => None, + }) + .collect(); + + // For ado blocks with no bindings, we check let statements and then + // apply pure to the expression. + // + // pure_type := a -> f a + // expression := t + if binder_types.is_empty() { + for step in &steps { + if let AdoStep::Let { statement, statements } = step { + state.with_error_crumb(ErrorCrumb::CheckingAdoLet(*statement), |state| { + form_let::check_let_chunks(state, context, statements) + })?; + } + } + return if let Some(expression) = expression { + let pure_type = form_do::lookup_or_synthesise_pure(state, context, pure)?; + application::check_function_term_application(state, context, pure_type, expression) + } else { + state.insert_error(ErrorKind::EmptyAdoBlock); + Ok(context.unknown("empty ado block")) + }; + } + + // Create a fresh unification variable for the in_expression. + // Inferring expression directly may solve the unification variables + // introduced in the first pass. This is undesirable, because the + // errors would be attributed incorrectly to the ado statements + // rather than the in-expression itself. + // + // ado + // a <- pure "Hello!" + // _ <- pure 42 + // in Message a + // + // in_expression :: Effect Message + // in_expression_type := ?in_expression + // lambda_type := ?a -> ?b -> ?in_expression + let in_expression_type = state.fresh_unification(context.queries, context.prim.t); + let lambda_type = + context.intern_function_chain(binder_types.iter().copied(), in_expression_type); + + // The desugared form of an ado-expression is a forward applicative + // pipeline, unlike do-notation which works inside-out. The example + // above desugars to the following expression: + // + // (\a _ -> Message a) <$> (pure "Hello!") <*> (pure 42) + // + // To emulate this, we process steps in source order. Let bindings + // are checked inline between map/apply operations. The first action + // uses infer_ado_map, and subsequent actions use infer_ado_apply. + // + // map_type :: (a -> b) -> f a -> f b + // lambda_type := ?a -> ?b -> ?in_expression + // + // expression_type := Effect String + // map(lambda, expression) := Effect (?b -> ?in_expression) + // >> + // >> ?a := String + // + // continuation_type := Effect (?b -> ?in_expression) + + // Lazily compute map_type and apply_type only when needed. + // - 1 action: only map is needed + // - 2+ actions: map and apply are needed + let action_count = binder_types.len(); + + let map_type = form_do::lookup_or_synthesise_map(state, context, map)?; + + let apply_type = if action_count > 1 { + form_do::lookup_or_synthesise_apply(state, context, apply)? + } else { + context.unknown("unused apply") + }; + + let mut continuation_type = None; + + for step in &steps { + match step { + AdoStep::Let { statement, statements } => { + state.with_error_crumb(ErrorCrumb::CheckingAdoLet(*statement), |state| { + form_let::check_let_chunks(state, context, statements) + })?; + } + AdoStep::Action { statement, expression, .. } => { + let statement_type = if let Some(continuation_type) = continuation_type { + // Then, the infer_ado_apply rule applies `apply` to the inferred + // expression type and the continuation type that is a function + // contained within some container, like Effect. + // + // apply_type := f (x -> y) -> f x -> f y + // continuation_type := Effect (?b -> ?in_expression) + // + // expression_type := Effect Int + // apply(continuation, expression) := Effect ?in_expression + // >> + // >> ?b := Int + // + // continuation_type := Effect ?in_expression + state.with_error_crumb(ErrorCrumb::InferringAdoApply(*statement), |state| { + infer_ado_apply_core( + state, + context, + apply_type, + continuation_type, + *expression, + ) + })? + } else { + state.with_error_crumb(ErrorCrumb::InferringAdoMap(*statement), |state| { + infer_ado_map_core(state, context, map_type, lambda_type, *expression) + })? + }; + continuation_type = Some(statement_type); + } + } + } + + // Finally, check the in-expression against in_expression. + // At this point the binder unification variables have been solved + // to concrete types, so errors are attributed to the in-expression. + // + // in_expression :: Effect Message + // in_expression_type := Effect ?in_expression + // >> + // >> ?in_expression := Message + if let Some(expression) = expression { + super::check_expression(state, context, expression, in_expression_type)?; + } + + let Some(continuation_type) = continuation_type else { + unreachable!("invariant violated: impossible empty steps"); + }; + + Ok(continuation_type) +} + +pub fn infer_ado_map_core( + state: &mut CheckState, + context: &CheckContext, + map_type: TypeId, + lambda_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expression_type = super::infer_expression(state, context, expression)?; + + let map_applied = application::check_function_application_core( + state, + context, + map_type, + lambda_type, + |state, context, lambda_type, expected_type| { + unification::subtype(state, context, lambda_type, expected_type)?; + Ok(lambda_type) + }, + )?; + + application::check_function_application_core( + state, + context, + map_applied, + expression_type, + |state, context, expression_type, expected_type| { + unification::subtype(state, context, expression_type, expected_type)?; + Ok(expression_type) + }, + ) +} + +pub fn infer_ado_apply_core( + state: &mut CheckState, + context: &CheckContext, + apply_type: TypeId, + continuation_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expression_type = super::infer_expression(state, context, expression)?; + + let apply_applied = application::check_function_application_core( + state, + context, + apply_type, + continuation_type, + |state, context, continuation_type, expected_type| { + unification::subtype(state, context, continuation_type, expected_type)?; + Ok(continuation_type) + }, + )?; + + application::check_function_application_core( + state, + context, + apply_applied, + expression_type, + |state, context, expression_type, expected_type| { + unification::subtype(state, context, expression_type, expected_type)?; + Ok(expression_type) + }, + ) +} diff --git a/compiler-core/checking2/src/source/terms/form_do.rs b/compiler-core/checking2/src/source/terms/form_do.rs new file mode 100644 index 00000000..4b2288fb --- /dev/null +++ b/compiler-core/checking2/src/source/terms/form_do.rs @@ -0,0 +1,439 @@ +use std::iter; + +use building_types::QueryResult; +use itertools::{Itertools, Position}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{TypeId, toolkit, unification}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::binder; +use crate::source::terms::{application, form_let}; +use crate::state::CheckState; + +enum DoStep<'a> { + Bind { + statement: lowering::DoStatementId, + binder_type: TypeId, + expression: Option, + }, + Discard { + statement: lowering::DoStatementId, + expression: Option, + }, + Let { + statement: lowering::DoStatementId, + statements: &'a [lowering::LetBindingChunk], + }, +} + +/// Lookup `bind` from resolution, or synthesize `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. +pub fn lookup_or_synthesise_bind( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(resolution) = resolution { + toolkit::lookup_term_variable(state, context, resolution) + } else { + let m = state.fresh_unification(context.queries, context.prim.type_to_type); + let a = state.fresh_unification(context.queries, context.prim.t); + let b = state.fresh_unification(context.queries, context.prim.t); + let m_a = context.intern_application(m, a); + let m_b = context.intern_application(m, b); + let a_to_m_b = context.intern_function(a, m_b); + Ok(context.intern_function_chain([m_a, a_to_m_b].into_iter(), m_b)) + } +} + +/// Lookup `discard` from resolution, or synthesize `?m ?a -> (?a -> ?m ?b) -> ?m ?b`. +pub fn lookup_or_synthesise_discard( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + // Same shape as bind + lookup_or_synthesise_bind(state, context, resolution) +} + +/// Lookup `map` from resolution, or synthesize `(?a -> ?b) -> ?f ?a -> ?f ?b`. +pub fn lookup_or_synthesise_map( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(resolution) = resolution { + toolkit::lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification(context.queries, context.prim.type_to_type); + let a = state.fresh_unification(context.queries, context.prim.t); + let b = state.fresh_unification(context.queries, context.prim.t); + let f_a = context.intern_application(f, a); + let f_b = context.intern_application(f, b); + let a_to_b = context.intern_function(a, b); + Ok(context.intern_function_chain([a_to_b, f_a].into_iter(), f_b)) + } +} + +/// Lookup `apply` from resolution, or synthesize `?f (?a -> ?b) -> ?f ?a -> ?f ?b`. +pub fn lookup_or_synthesise_apply( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(resolution) = resolution { + toolkit::lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification(context.queries, context.prim.type_to_type); + let a = state.fresh_unification(context.queries, context.prim.t); + let b = state.fresh_unification(context.queries, context.prim.t); + let a_to_b = context.intern_function(a, b); + let f_a_to_b = context.intern_application(f, a_to_b); + let f_a = context.intern_application(f, a); + let f_b = context.intern_application(f, b); + Ok(context.intern_function_chain([f_a_to_b, f_a].into_iter(), f_b)) + } +} + +/// Lookup `pure` from resolution, or synthesize `?a -> ?f ?a`. +pub fn lookup_or_synthesise_pure( + state: &mut CheckState, + context: &CheckContext, + resolution: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(resolution) = resolution { + toolkit::lookup_term_variable(state, context, resolution) + } else { + let f = state.fresh_unification(context.queries, context.prim.type_to_type); + let a = state.fresh_unification(context.queries, context.prim.t); + let f_a = context.intern_application(f, a); + Ok(context.intern_function(a, f_a)) + } +} + +pub fn infer_do( + state: &mut CheckState, + context: &CheckContext, + bind: Option, + discard: Option, + statement_ids: &[lowering::DoStatementId], +) -> QueryResult +where + Q: ExternalQueries, +{ + // First, perform a forward pass where variable bindings are bound + // to unification variables. Let bindings are not checked here to + // avoid premature solving of unification variables. Instead, they + // are checked inline during the statement checking loop. + let mut steps = vec![]; + for &statement_id in statement_ids.iter() { + let Some(statement) = context.lowered.info.get_do_statement(statement_id) else { + continue; + }; + match statement { + lowering::DoStatement::Bind { binder, expression } => { + let binder_type = if let Some(binder) = binder { + binder::infer_binder(state, context, *binder)? + } else { + state.fresh_unification(context.queries, context.prim.t) + }; + steps.push(DoStep::Bind { + statement: statement_id, + binder_type, + expression: *expression, + }); + } + lowering::DoStatement::Let { statements } => { + steps.push(DoStep::Let { statement: statement_id, statements }); + } + lowering::DoStatement::Discard { expression } => { + steps.push(DoStep::Discard { statement: statement_id, expression: *expression }); + } + } + } + + let action_count = steps + .iter() + .filter(|step| matches!(step, DoStep::Bind { .. } | DoStep::Discard { .. })) + .count(); + + let (has_bind_step, has_discard_step) = { + let mut has_bind = false; + let mut has_discard = false; + for (position, statement) in steps.iter().with_position() { + let is_final = matches!(position, Position::Last | Position::Only); + match statement { + DoStep::Bind { .. } => has_bind = true, + DoStep::Discard { .. } if !is_final => has_discard = true, + _ => (), + } + } + (has_bind, has_discard) + }; + + let bind_type = if has_bind_step { + lookup_or_synthesise_bind(state, context, bind)? + } else { + context.unknown("unused bind") + }; + + let discard_type = if has_discard_step { + lookup_or_synthesise_discard(state, context, discard)? + } else { + context.unknown("unused discard") + }; + + let pure_expression = match steps.iter().last() { + Some(statement) => match statement { + // Technically valid, syntactically disallowed. This allows + // partially-written do expressions to infer, with a friendly + // warning to nudge the user that `bind` is prohibited. + DoStep::Bind { statement, expression, .. } => { + state.with_error_crumb(ErrorCrumb::InferringDoBind(*statement), |state| { + state.insert_error(ErrorKind::InvalidFinalBind); + }); + expression + } + DoStep::Discard { expression, .. } => expression, + DoStep::Let { statement, .. } => { + state.with_error_crumb(ErrorCrumb::CheckingDoLet(*statement), |state| { + state.insert_error(ErrorKind::InvalidFinalLet); + }); + return Ok(context.unknown("invalid final let")); + } + }, + None => { + state.insert_error(ErrorKind::EmptyDoBlock); + return Ok(context.unknown("empty do block")); + } + }; + + // If either don't actually have expressions, it's empty! + let Some(pure_expression) = *pure_expression else { + state.insert_error(ErrorKind::EmptyDoBlock); + return Ok(context.unknown("empty do block")); + }; + + // Create unification variables that each statement in the do expression + // will unify against. The next section will get into more detail how + // these are used. These unification variables are used to enable GHC-like + // left-to-right checking of do expressions while maintaining the same + // semantics as rebindable `do` in PureScript. + let continuation_types = + iter::repeat_with(|| state.fresh_unification(context.queries, context.prim.t)) + .take(action_count) + .collect_vec(); + + // Let's trace over the following example: + // + // main = do + // a <- effect + // b <- aff + // pure { a, b } + // + // For the first statement, we know the following information. We + // instantiate the `bind` function to prepare it for application. + // The first argument is easy, it's just the expression_type; the + // second argument involves synthesising a function type using the + // `binder_type` and the `next` continuation. The application of + // these arguments creates important unifications, listed below. + // Additionally, we also create a unification to unify the `now` + // type with the result of the `bind` application. + // + // expression_type := Effect Int + // binder_type := ?a + // now := ?0 + // next := ?1 + // lambda_type := ?a -> ?1 + // + // bind_type := m a -> (a -> m b) -> m b + // | + // := apply(expression_type) + // := (Int -> Effect ?r1) -> Effect ?r1 + // | + // := apply(lambda_type) + // := Effect ?r1 + // | + // >> ?a := Int + // >> ?1 := Effect ?r1 + // >> ?0 := Effect ?r1 + // + // For the second statement, we know the following information. + // The `now` type was already solved by the previous statement, + // and an error should surface once we check the inferred type + // of the statement against it. + // + // expression_type := Aff Int + // binder_type := ?b + // now := ?1 := Effect ?r1 + // next := ?2 + // lambda_type := ?b -> ?2 + // + // bind_type := m a -> (a -> m b) -> m b + // | + // := apply(expression_type) + // := (Int -> Aff ?r2) -> Aff ?r2 + // | + // := apply(lambda_type) + // := Aff ?r2 + // | + // >> ?b := Int + // >> ?2 := Aff ?r2 + // | + // >> ?1 ~ Aff ?r2 + // >> Effect ?r1 ~ Aff ?r2 + // | + // >> Oh no! + // + // This unification error is expected, but this left-to-right checking + // approach significantly improves the reported error positions compared + // to the previous approach that emulated desugared checking. + + let mut continuations = continuation_types.iter().tuple_windows::<(_, _)>(); + + for step in &steps { + match step { + DoStep::Let { statement, statements } => { + state.with_error_crumb(ErrorCrumb::CheckingDoLet(*statement), |state| { + form_let::check_let_chunks(state, context, statements) + })?; + } + DoStep::Bind { statement, binder_type, expression } => { + let Some((&now_type, &next_type)) = continuations.next() else { + continue; + }; + let Some(expression) = *expression else { + continue; + }; + state.with_error_crumb(ErrorCrumb::InferringDoBind(*statement), |state| { + let statement_type = infer_do_bind_core( + state, + context, + bind_type, + next_type, + expression, + *binder_type, + )?; + unification::subtype(state, context, statement_type, now_type)?; + Ok(()) + })?; + } + DoStep::Discard { statement, expression } => { + let Some((&now_type, &next_type)) = continuations.next() else { + continue; + }; + let Some(expression) = *expression else { + continue; + }; + state.with_error_crumb(ErrorCrumb::InferringDoDiscard(*statement), |state| { + let statement_type = + infer_do_discard_core(state, context, discard_type, next_type, expression)?; + unification::subtype(state, context, statement_type, now_type)?; + Ok(()) + })?; + } + } + } + + // The `first_continuation` is the overall type of the do expression, + // built iteratively and through solving unification variables. On + // the other hand, the `final_continuation` is the expected type for + // the final statement in the do expression. If there is only a single + // statement in the do expression, then these two bindings are equivalent. + let first_continuation = + *continuation_types.first().expect("invariant violated: empty continuation_types"); + let final_continuation = + *continuation_types.last().expect("invariant violated: empty continuation_types"); + + super::check_expression(state, context, pure_expression, final_continuation)?; + + Ok(first_continuation) +} + +pub fn infer_do_bind_core( + state: &mut CheckState, + context: &CheckContext, + bind_type: TypeId, + continuation_type: TypeId, + expression: lowering::ExpressionId, + binder_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expression_type = super::infer_expression(state, context, expression)?; + let lambda_type = context.intern_function(binder_type, continuation_type); + + let bind_applied = application::check_function_application_core( + state, + context, + bind_type, + expression_type, + |state, context, expression_type, expected_type| { + unification::subtype(state, context, expression_type, expected_type)?; + Ok(expression_type) + }, + )?; + + application::check_function_application_core( + state, + context, + bind_applied, + lambda_type, + |state, context, lambda_type, expected_type| { + unification::subtype(state, context, lambda_type, expected_type)?; + Ok(lambda_type) + }, + ) +} + +pub fn infer_do_discard_core( + state: &mut CheckState, + context: &CheckContext, + discard_type: TypeId, + continuation_type: TypeId, + expression: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let expression_type = super::infer_expression(state, context, expression)?; + + let discard_applied = application::check_function_application_core( + state, + context, + discard_type, + expression_type, + |state, context, expression_type, expected_type| { + unification::subtype(state, context, expression_type, expected_type)?; + Ok(expression_type) + }, + )?; + + let result_type = application::check_function_application_core( + state, + context, + discard_applied, + (), + |_, _, _, continuation_type| Ok(continuation_type), + )?; + + unification::subtype(state, context, continuation_type, result_type)?; + + Ok(continuation_type) +} diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs new file mode 100644 index 00000000..92a36339 --- /dev/null +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -0,0 +1,163 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorCrumb; +use crate::source::terms::equations; +use crate::source::{binder, types}; +use crate::state::CheckState; + +pub fn check_let_chunks( + state: &mut CheckState, + context: &CheckContext, + chunks: &[lowering::LetBindingChunk], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for chunk in chunks { + match chunk { + lowering::LetBindingChunk::Pattern { binder, where_expression } => { + check_pattern_let_binding(state, context, binder, where_expression)?; + } + lowering::LetBindingChunk::Names { bindings, scc } => { + check_names_chunk(state, context, bindings, scc)?; + } + } + } + Ok(()) +} + +pub fn check_pattern_let_binding( + state: &mut CheckState, + context: &CheckContext, + binder: &Option, + where_expression: &Option, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(where_expression) = where_expression else { + return Ok(()); + }; + + let expression_type = equations::infer_where_expression(state, context, where_expression)?; + + let Some(binder) = *binder else { + return Ok(()); + }; + + let expression_type = if binder::requires_instantiation(context, binder) { + toolkit::instantiate_unifications(state, context, expression_type)? + } else { + expression_type + }; + + binder::check_binder(state, context, binder, expression_type)?; + + Ok(()) +} + +pub fn check_names_chunk( + state: &mut CheckState, + context: &CheckContext, + bindings: &[lowering::LetBindingNameGroupId], + scc: &[lowering::Scc], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &id in bindings { + let Some(name) = context.lowered.info.get_let_binding(id) else { + continue; + }; + if let Some(signature_id) = name.signature { + let (name_type, _) = types::check_kind(state, context, signature_id, context.prim.t)?; + state.checked.nodes.lets.insert(id, name_type); + } else { + let name_type = state.fresh_unification(context.queries, context.prim.t); + state.checked.nodes.lets.insert(id, name_type); + } + } + + for item in scc { + match item { + lowering::Scc::Base(id) | lowering::Scc::Recursive(id) => { + check_let_name_binding(state, context, *id)?; + } + lowering::Scc::Mutual(mutual) => { + for id in mutual { + check_let_name_binding(state, context, *id)?; + } + } + } + } + + Ok(()) +} + +pub fn check_let_name_binding( + state: &mut CheckState, + context: &CheckContext, + id: lowering::LetBindingNameGroupId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_implication(|state| { + state.with_error_crumb(ErrorCrumb::CheckingLetName(id), |state| { + check_let_name_binding_core(state, context, id) + }) + }) +} + +pub fn check_let_name_binding_core( + state: &mut CheckState, + context: &CheckContext, + id: lowering::LetBindingNameGroupId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(name) = context.lowered.info.get_let_binding(id) else { + return Ok(()); + }; + + let Some(name_type) = state.checked.nodes.lookup_let(id) else { + return Ok(()); + }; + + if let Some(signature_id) = name.signature { + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, name_type)?; + + let toolkit::InspectFunction { arguments, result } = + toolkit::inspect_function(state, context, quantified)?; + + let function = context.intern_function_chain(arguments.iter().copied(), result); + + equations::check_equations_core( + state, + context, + signature_id, + &arguments, + result, + function, + &name.equations, + )?; + } else { + // Keep simple let bindings e.g. `bind = ibind` polymorphic. + if let [equation] = name.equations.as_ref() + && equation.binders.is_empty() + && let Some(guarded) = &equation.guarded + { + let inferred_type = equations::infer_guarded_expression(state, context, guarded)?; + state.checked.nodes.lets.insert(id, inferred_type); + } else { + equations::infer_equations_core(state, context, name_type, &name.equations)?; + } + } + + Ok(()) +} diff --git a/compiler-core/checking2/src/source/terms/forms.rs b/compiler-core/checking2/src/source/terms/forms.rs new file mode 100644 index 00000000..11d3d6d8 --- /dev/null +++ b/compiler-core/checking2/src/source/terms/forms.rs @@ -0,0 +1,225 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{TypeId, toolkit, unification}; +use crate::source::terms::{equations, form_let}; +use crate::source::{binder, terms}; +use crate::state::CheckState; + +pub fn infer_if_then_else( + state: &mut CheckState, + context: &CheckContext, + if_: Option, + then: Option, + else_: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(if_) = if_ { + super::check_expression(state, context, if_, context.prim.boolean)?; + } + + let result_type = state.fresh_unification(context.queries, context.prim.t); + + if let Some(then) = then { + super::check_expression(state, context, then, result_type)?; + } + + if let Some(else_) = else_ { + super::check_expression(state, context, else_, result_type)?; + } + + Ok(result_type) +} + +pub fn check_if_then_else( + state: &mut CheckState, + context: &CheckContext, + if_: Option, + then: Option, + else_: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + if let Some(if_) = if_ { + super::check_expression(state, context, if_, context.prim.boolean)?; + } + + if let Some(then) = then { + super::check_expression(state, context, then, expected)?; + } + + if let Some(else_) = else_ { + super::check_expression(state, context, else_, expected)?; + } + + Ok(expected) +} + +pub fn infer_lambda( + state: &mut CheckState, + context: &CheckContext, + binders: &[lowering::BinderId], + expression: Option, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut argument_types = vec![]; + + for &binder_id in binders.iter() { + let argument_type = state.fresh_unification(context.queries, context.prim.t); + binder::check_binder(state, context, binder_id, argument_type)?; + argument_types.push(argument_type); + } + + let result_type = if let Some(body) = expression { + let body_type = super::infer_expression(state, context, body)?; + toolkit::instantiate_constrained(state, context, body_type)? + } else { + state.fresh_unification(context.queries, context.prim.t) + }; + + Ok(context.intern_function_chain(argument_types.iter().copied(), result_type)) +} + +pub fn check_lambda( + state: &mut CheckState, + context: &CheckContext, + binders: &[lowering::BinderId], + expression: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + let mut remaining = expected; + + for &binder_id in binders.iter() { + let decomposed = toolkit::decompose_function(state, context, remaining)?; + if let Some((argument, result)) = decomposed { + binder::check_binder(state, context, binder_id, argument)?; + arguments.push(argument); + remaining = result; + } else { + let argument_type = state.fresh_unification(context.queries, context.prim.t); + binder::check_binder(state, context, binder_id, argument_type)?; + arguments.push(argument_type); + } + } + + let result_type = if let Some(body) = expression { + super::check_expression(state, context, body, remaining)? + } else { + state.fresh_unification(context.queries, context.prim.t) + }; + + Ok(context.intern_function_chain(arguments.iter().copied(), result_type)) +} + +pub fn instantiate_trunk_types( + state: &mut CheckState, + context: &CheckContext, + trunk_types: &mut [TypeId], + branches: &[lowering::CaseBranch], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for (position, trunk_type) in trunk_types.iter_mut().enumerate() { + let should_instantiate = branches.iter().any(|branch| { + let binder = branch.binders.get(position); + binder.is_some_and(|&binder_id| binder::requires_instantiation(context, binder_id)) + }); + if should_instantiate { + *trunk_type = toolkit::instantiate_unifications(state, context, *trunk_type)?; + } + } + Ok(()) +} + +pub fn infer_case_of( + state: &mut CheckState, + context: &CheckContext, + trunk: &[lowering::ExpressionId], + branches: &[lowering::CaseBranch], +) -> QueryResult +where + Q: ExternalQueries, +{ + let result_type = state.fresh_unification(context.queries, context.prim.t); + + let mut trunk_types = vec![]; + for trunk in trunk.iter() { + let trunk_type = super::infer_expression(state, context, *trunk)?; + trunk_types.push(trunk_type); + } + + instantiate_trunk_types(state, context, &mut trunk_types, branches)?; + + for branch in branches.iter() { + for (binder, trunk) in branch.binders.iter().zip(&trunk_types) { + binder::check_binder(state, context, *binder, *trunk)?; + } + if let Some(guarded) = &branch.guarded_expression { + let guarded_type = equations::infer_guarded_expression(state, context, guarded)?; + unification::subtype(state, context, guarded_type, result_type)?; + } + } + + Ok(result_type) +} + +pub fn check_case_of( + state: &mut CheckState, + context: &CheckContext, + trunk: &[lowering::ExpressionId], + branches: &[lowering::CaseBranch], + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut trunk_types = vec![]; + for trunk in trunk.iter() { + let trunk_type = super::infer_expression(state, context, *trunk)?; + trunk_types.push(trunk_type); + } + + instantiate_trunk_types(state, context, &mut trunk_types, branches)?; + + for branch in branches.iter() { + for (binder, trunk) in branch.binders.iter().zip(&trunk_types) { + binder::check_binder(state, context, *binder, *trunk)?; + } + if let Some(guarded) = &branch.guarded_expression { + equations::check_guarded_expression(state, context, guarded, expected)?; + } + } + + Ok(expected) +} + +pub fn check_let_in( + state: &mut CheckState, + context: &CheckContext, + bindings: &[lowering::LetBindingChunk], + expression: Option, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + form_let::check_let_chunks(state, context, bindings)?; + + let Some(expression) = expression else { + return Ok(context.unknown("missing let expression")); + }; + + terms::check_expression(state, context, expression, expected) +} From 7b3744e29ca3d40e62b9f785904ab4be499e8062 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 00:54:09 +0800 Subject: [PATCH 270/386] Format files and apply clippy fixes --- compiler-core/checking2/src/source/binder.rs | 10 +++++++--- compiler-core/checking2/src/source/terms/form_do.rs | 6 +++--- tests-integration/src/generated/basic.rs | 3 +-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs index 2283c188..af8a3c8c 100644 --- a/compiler-core/checking2/src/source/binder.rs +++ b/compiler-core/checking2/src/source/binder.rs @@ -43,7 +43,12 @@ where Q: ExternalQueries, { state.with_error_crumb(ErrorCrumb::CheckingBinder(binder_id), |state| { - binder_core(state, context, binder_id, BinderMode::Check { expected_type, elaborating: true }) + binder_core( + state, + context, + binder_id, + BinderMode::Check { expected_type, elaborating: true }, + ) }) } @@ -357,8 +362,7 @@ where let binder_kind = normalise::normalise(state, context, binder.kind)?; let replacement = state.fresh_unification(context.queries, binder_kind); - let function_t = - SubstituteName::one(state, context, binder.name, replacement, inner)?; + let function_t = SubstituteName::one(state, context, binder.name, replacement, inner)?; check_function_application_core(state, context, function_t, argument_id, check_argument) } diff --git a/compiler-core/checking2/src/source/terms/form_do.rs b/compiler-core/checking2/src/source/terms/form_do.rs index 4b2288fb..5b14659b 100644 --- a/compiler-core/checking2/src/source/terms/form_do.rs +++ b/compiler-core/checking2/src/source/terms/form_do.rs @@ -45,7 +45,7 @@ where let m_a = context.intern_application(m, a); let m_b = context.intern_application(m, b); let a_to_m_b = context.intern_function(a, m_b); - Ok(context.intern_function_chain([m_a, a_to_m_b].into_iter(), m_b)) + Ok(context.intern_function_chain([m_a, a_to_m_b], m_b)) } } @@ -80,7 +80,7 @@ where let f_a = context.intern_application(f, a); let f_b = context.intern_application(f, b); let a_to_b = context.intern_function(a, b); - Ok(context.intern_function_chain([a_to_b, f_a].into_iter(), f_b)) + Ok(context.intern_function_chain([a_to_b, f_a], f_b)) } } @@ -103,7 +103,7 @@ where let f_a_to_b = context.intern_application(f, a_to_b); let f_a = context.intern_application(f, a); let f_b = context.intern_application(f, b); - Ok(context.intern_function_chain([f_a_to_b, f_a].into_iter(), f_b)) + Ok(context.intern_function_chain([f_a_to_b, f_a], f_b)) } } diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 481b567c..af19978d 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -243,8 +243,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(definition) = checked.lookup_synonym(id) else { continue }; - let names = - definition.parameters.iter().map(|b| (b.name, engine.lookup_smol_str(b.text))); + let names = definition.parameters.iter().map(|b| (b.name, engine.lookup_smol_str(b.text))); let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); let binders = definition.parameters.iter().map(|b| engine.lookup_smol_str(b.text)).collect_vec(); From 13e28a84a18014920090b993a24b6b5197bd34f8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 02:07:39 +0800 Subject: [PATCH 271/386] Port exhaustiveness checking to checking2 --- compiler-core/checking2/src/core.rs | 1 + .../checking2/src/core/exhaustive.rs | 1262 +++++++++++++++++ .../checking2/src/core/exhaustive/convert.rs | 382 +++++ .../checking2/src/core/exhaustive/pretty.rs | 172 +++ compiler-core/checking2/src/source/binder.rs | 5 +- .../checking2/src/source/operator.rs | 129 +- .../checking2/src/source/terms/equations.rs | 12 +- .../checking2/src/source/terms/form_let.rs | 14 +- .../checking2/src/source/terms/forms.rs | 49 +- compiler-core/checking2/src/state.rs | 45 + 10 files changed, 2057 insertions(+), 14 deletions(-) create mode 100644 compiler-core/checking2/src/core/exhaustive.rs create mode 100644 compiler-core/checking2/src/core/exhaustive/convert.rs create mode 100644 compiler-core/checking2/src/core/exhaustive/pretty.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 6682c39d..81585629 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -1,5 +1,6 @@ //! Implements core type structures. +pub mod exhaustive; pub mod fold; pub mod generalise; pub mod normalise; diff --git a/compiler-core/checking2/src/core/exhaustive.rs b/compiler-core/checking2/src/core/exhaustive.rs new file mode 100644 index 00000000..e43308f3 --- /dev/null +++ b/compiler-core/checking2/src/core/exhaustive.rs @@ -0,0 +1,1262 @@ +mod convert; +mod pretty; + +use std::iter; + +use building_types::QueryResult; +use files::FileId; +use indexing::TermItemId; +use itertools::Itertools; +use rustc_hash::FxHashSet; +use smol_str::SmolStr; + +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, toolkit}; +use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Pattern { + pub kind: PatternKind, + pub t: TypeId, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PatternKind { + Wildcard, + Constructor { constructor: PatternConstructor }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PatternConstructor { + DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Record { labels: Vec, fields: Vec }, + Array { fields: Vec }, + Boolean(bool), + Integer(i32), + Number(bool, SmolStr), + String(SmolStr), + Char(char), +} + +impl PatternConstructor { + /// Returns the arity of this pattern constructor. + /// + /// [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`] have non-zero arity based on their fields. + pub fn arity(&self) -> usize { + match self { + PatternConstructor::DataConstructor { fields, .. } => fields.len(), + PatternConstructor::Record { fields, .. } => fields.len(), + PatternConstructor::Array { fields } => fields.len(), + _ => 0, + } + } + + /// Returns the fields of this pattern constructor. + /// + /// [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`] have fields corresponding to their arguments. + pub fn fields(&self) -> &[PatternId] { + match self { + PatternConstructor::DataConstructor { fields, .. } => fields, + PatternConstructor::Record { fields, .. } => fields, + PatternConstructor::Array { fields } => fields, + _ => &[], + } + } + + /// Checks if a pattern constructor matches another. + /// + /// This is used during the specialisation algorithm to determine if a + /// pattern row should be included in the specialised pattern matrix. + pub fn matches(&self, other: &PatternConstructor) -> bool { + match (self, other) { + ( + PatternConstructor::DataConstructor { file_id: f1, item_id: i1, .. }, + PatternConstructor::DataConstructor { file_id: f2, item_id: i2, .. }, + ) => f1 == f2 && i1 == i2, + // Any record constructor matches any other record constructor + (PatternConstructor::Record { .. }, PatternConstructor::Record { .. }) => true, + // Array constructors match only when their lengths match + ( + PatternConstructor::Array { fields: f1 }, + PatternConstructor::Array { fields: f2 }, + ) => f1.len() == f2.len(), + (PatternConstructor::Boolean(b1), PatternConstructor::Boolean(b2)) => b1 == b2, + (PatternConstructor::Integer(i1), PatternConstructor::Integer(i2)) => i1 == i2, + (PatternConstructor::Number(n1, v1), PatternConstructor::Number(n2, v2)) => { + n1 == n2 && v1 == v2 + } + (PatternConstructor::String(s1), PatternConstructor::String(s2)) => s1 == s2, + (PatternConstructor::Char(c1), PatternConstructor::Char(c2)) => c1 == c2, + _ => false, + } + } + + /// Reconstructs a [`PatternConstructor`] with the given fields. + /// + /// For [`PatternConstructor::DataConstructor`], [`PatternConstructor::Record`], + /// and [`PatternConstructor::Array`], this function overrides the fields. + /// Otherwise, the fields must be empty as enforced by an assertion. + /// + /// This algorithm is used in [`algorithm_m`] to replace the fields of the + /// pattern constructor we're specialising on with the fields generated by + /// the witnesses. + pub fn reconstruct(&self, fields: &[PatternId]) -> PatternConstructor { + match *self { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + let fields = fields.to_vec(); + PatternConstructor::DataConstructor { file_id, item_id, fields } + } + PatternConstructor::Record { ref labels, .. } => { + let fields = fields.to_vec(); + PatternConstructor::Record { labels: labels.clone(), fields } + } + PatternConstructor::Array { .. } => { + let fields = fields.to_vec(); + PatternConstructor::Array { fields } + } + PatternConstructor::Boolean(b) => { + assert!(fields.is_empty(), "Boolean constructor has arity 0"); + PatternConstructor::Boolean(b) + } + PatternConstructor::Integer(i) => { + assert!(fields.is_empty(), "Integer constructor has arity 0"); + PatternConstructor::Integer(i) + } + PatternConstructor::Number(negative, ref n) => { + assert!(fields.is_empty(), "Number constructor has arity 0"); + PatternConstructor::Number(negative, SmolStr::clone(n)) + } + PatternConstructor::String(ref s) => { + assert!(fields.is_empty(), "String constructor has arity 0"); + PatternConstructor::String(SmolStr::clone(s)) + } + PatternConstructor::Char(c) => { + assert!(fields.is_empty(), "Char constructor has arity 0"); + PatternConstructor::Char(c) + } + } + } +} + +impl MissingConstructor { + /// Constructs a witness pattern for this missing constructor. + pub fn construct_missing_witness(&self, state: &mut CheckState, t: TypeId) -> PatternId { + match *self { + MissingConstructor::DataConstructor { file_id, item_id, ref fields } => { + let fields = fields + .iter() + .map(|&field_type| state.allocate_wildcard(field_type)) + .collect_vec(); + + let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields }; + let pattern = PatternKind::Constructor { constructor }; + + state.allocate_pattern(pattern, t) + } + MissingConstructor::Boolean(b) => { + let constructor = PatternConstructor::Boolean(b); + let pattern = PatternKind::Constructor { constructor }; + state.allocate_pattern(pattern, t) + } + } + } +} + +pub type PatternId = interner::Id; +pub type PatternStorage = interner::Interner; + +type PatternVector = Vec; +type PatternMatrix = Vec; +pub type WitnessVector = Vec; + +/// Determines if a [`PatternVector`] is useful with respect to a [`PatternMatrix`]. +/// +/// A pattern vector is useful if it matches at least one value not matched by +/// any pattern vector in the matrix. This is one of the core algorithms from +/// Maranget's "Warnings for pattern matching" paper. +/// +/// See [`algorithm_u_constructor`] and [`algorithm_u_wildcard`] for reference. +fn algorithm_u( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult +where + Q: ExternalQueries, +{ + // Base case: any pattern is useful against an empty matrix + if matrix.is_empty() { + return Ok(true); + } + + // Base case: an empty pattern vector against non-empty matrix is useless + let [first_pattern, ..] = vector[..] else { + return Ok(false); + }; + + let first_pattern = state.patterns[first_pattern].clone(); + + match first_pattern.kind { + PatternKind::Constructor { constructor } => { + algorithm_u_constructor(state, context, matrix, vector, constructor) + } + PatternKind::Wildcard => { + algorithm_u_wildcard(state, context, matrix, vector, first_pattern.t) + } + } +} + +/// Induction 1 +/// +/// This function uses specialisation to spread the provided [`PatternConstructor`] +/// over both the [`PatternMatrix`] and the [`PatternVector`], before calling +/// [`algorithm_u`] recursively with the specialised structures. +fn algorithm_u_constructor( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + constructor: PatternConstructor, +) -> QueryResult +where + Q: ExternalQueries, +{ + let constructor = canonicalise_record_constructor(state, constructor, matrix); + let specialised_matrix = specialise_matrix(state, &constructor, matrix); + + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { + unreachable!("invariant violated: vector contains constructor"); + }; + + algorithm_u(state, context, &specialised_matrix, &specialised_vector) +} + +/// Induction 2 +/// +/// This function collects all constructor references from the first column of +/// the matrix into a collection called the sigma. +/// +/// If the sigma is complete, for each constructor in the sigma, we specialise +/// the pattern matrix and pattern vector against it. Then, we recursively call +/// [`algorithm_u`] against the specialised structures. The pattern vector is +/// useful if any specialised pattern vector is useful against its specialised +/// pattern matrix. +/// +/// If the sigma is incomplete, we recursively call [`algorithm_u`] against the +/// [`default_matrix`] of the pattern matrix and the tail of the pattern vector. +fn algorithm_u_wildcard( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let sigma = collect_sigma(state, context, matrix, t)?; + let complete = sigma_is_complete(context, &sigma)?; + + if complete { + algorithm_u_wildcard_complete(state, context, matrix, vector, sigma) + } else { + algorithm_u_wildcard_incomplete(state, context, matrix, vector) + } +} + +fn algorithm_u_wildcard_complete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + sigma: Sigma, +) -> QueryResult +where + Q: ExternalQueries, +{ + for constructor in sigma.constructors { + let specialised_matrix = specialise_matrix(state, &constructor, matrix); + + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { + unreachable!("invariant violated: vector contains constructor"); + }; + + if algorithm_u(state, context, &specialised_matrix, &specialised_vector)? { + return Ok(true); + } + } + Ok(false) +} + +fn algorithm_u_wildcard_incomplete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult +where + Q: ExternalQueries, +{ + let default = default_matrix(state, matrix); + let tail_columns = vector[1..].to_vec(); + algorithm_u(state, context, &default, &tail_columns) +} + +/// Determines the matching [`WitnessVector`] given a [`PatternMatrix`] +/// and some [`PatternVector`]. +/// +/// If the pattern vector is useful against the provided matrix, that is, +/// there are cases yet to be covered, this function will return a non-empty +/// list of witnesses. Inversely, if the pattern vector is useless against +/// the provided matrix, that is, the cases are exhaustive, this function +/// will return [`None`]. +/// +/// So... what exactly are witnesses? In the paper, these are defined as +/// 'value vectors' that are known not to be matched against the pattern +/// matrix but are instantiations of the pattern vector. In our implementation, +/// these witnesses are pattern vectors that denote values not yet covered by +/// the matrix. +/// +/// The [`algorithm_m_wildcard`] induction is prolific for producing these +/// these witnesses as it compares the constructors that appear in the +/// matrix against the constructors available in the checking environment. +fn algorithm_m( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + // Base case: any pattern is its own witness against an empty matrix + if matrix.is_empty() { + let vector = vector.clone(); + return Ok(Some(vec![vector])); + } + + // Base case: an empty pattern vector against non-empty matrix has no witnesses + let [first_pattern, ..] = vector[..] else { + return Ok(None); + }; + + let first_pattern = state.patterns[first_pattern].clone(); + + match first_pattern.kind { + PatternKind::Constructor { constructor } => { + algorithm_m_constructor(state, context, matrix, vector, constructor, first_pattern.t) + } + PatternKind::Wildcard => { + algorithm_m_wildcard(state, context, matrix, vector, first_pattern.t) + } + } +} + +/// Induction 1 +/// +/// This function uses specialisation to spread the provided [`PatternConstructor`] +/// over both the [`PatternMatrix`] and the [`PatternVector`], before calling +/// [`algorithm_m`] recursively with the specialised structures. +/// +/// The final set of witnesses returned by this induction includes a +/// reconstruction of the original constructor passed to this function. +/// +/// See documentation for [`specialise_matrix`] and [`specialise_vector`] for +/// more information on what specialisation entails given a constructor. +fn algorithm_m_constructor( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + constructor: PatternConstructor, + t: TypeId, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let constructor = canonicalise_record_constructor(state, constructor, matrix); + let arity = constructor.arity(); + + let specialised_matrix = specialise_matrix(state, &constructor, matrix); + + let Some(specialised_vector) = specialise_vector(state, &constructor, vector) else { + unreachable!("invariant violated: vector contains constructor"); + }; + + let witnesses = algorithm_m(state, context, &specialised_matrix, &specialised_vector)?; + + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + let witnesses = witnesses.into_iter().map(|witness| { + let (argument_columns, tail_columns) = witness.split_at(arity); + + let constructor = constructor.reconstruct(argument_columns); + let constructor_id = state.allocate_constructor(constructor, t); + let tail_columns = tail_columns.iter().copied(); + + iter::once(constructor_id).chain(tail_columns).collect() + }); + + let witnesses = witnesses.collect(); + Ok(Some(witnesses)) +} + +/// Induction 2 +/// +/// If the first column in the [`PatternVector`] is a wildcard, this function +/// produces witnesses that correspond to patterns not yet covered by the +/// [`PatternMatrix`]. This is where pattern suggestion warnings are built +/// for the compiler! +/// +/// This function collects all constructor references from the first column +/// of all rows in the matrix into a collection called the sigma. We handle +/// the structure in different ways: +/// +/// If the sigma is complete, for each constructor in the sigma, we apply +/// a rule similar to [`algorithm_m_constructor`] to collect witnesses +/// across all constructors. +/// +/// If the sigma is incomplete, we recursively apply [`algorithm_m`] to the +/// [`default_matrix`] of the pattern matrix and the tail columns of the +/// pattern vector. The induction ends if the recursive call is exhaustive. +/// +/// If the recursive call returns witnesses, and the sigma is non-empty, +/// we move our attention to generating [`Constructor`] patterns for +/// constructors not present in the sigma. This is what we use for +/// reporting pattern warnings. Otherwise, if the sigma is empty, we +/// simply produce a wildcard pattern. +fn algorithm_m_wildcard( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + t: TypeId, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let sigma = collect_sigma(state, context, matrix, t)?; + let complete = sigma_is_complete(context, &sigma)?; + if complete { + algorithm_m_wildcard_complete(state, context, matrix, vector, t, &sigma) + } else { + algorithm_m_wildcard_incomplete(state, context, matrix, vector, t, &sigma) + } +} + +fn algorithm_m_wildcard_complete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + t: TypeId, + sigma: &Sigma, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let mut all_witnesses = vec![]; + + for constructor in &sigma.constructors { + let arity = constructor.arity(); + + let specialised_matrix = specialise_matrix(state, constructor, matrix); + + let Some(specialised_vector) = specialise_vector(state, constructor, vector) else { + unreachable!("invariant violated: vector contains constructor"); + }; + + if let Some(witnesses) = + algorithm_m(state, context, &specialised_matrix, &specialised_vector)? + { + for witness in witnesses { + let (argument_columns, tail_columns) = witness.split_at(arity); + + let constructor = constructor.reconstruct(argument_columns); + let constructor_id = state.allocate_constructor(constructor, t); + let tail_columns = tail_columns.iter().copied(); + + let witnesses = iter::once(constructor_id).chain(tail_columns).collect(); + all_witnesses.push(witnesses); + } + } + } + + if all_witnesses.is_empty() { Ok(None) } else { Ok(Some(all_witnesses)) } +} + +fn algorithm_m_wildcard_incomplete( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + vector: &PatternVector, + t: TypeId, + sigma: &Sigma, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let default = default_matrix(state, matrix); + let tail_columns = vector[1..].to_vec(); + + let witnesses = algorithm_m(state, context, &default, &tail_columns)?; + + let Some(witnesses) = witnesses else { + return Ok(None); + }; + + let first_column = if let Some(constructor) = sigma.missing.first() { + constructor.construct_missing_witness(state, t) + } else { + state.allocate_wildcard(t) + }; + + let witness = witnesses + .into_iter() + .map(|witness| iter::once(first_column).chain(witness).collect()) + .collect(); + + Ok(Some(witness)) +} + +/// Computes a canonical [`PatternConstructor::Record`] that includes the union of all +/// record labels appearing in the first column of the matrix and the given constructor. +/// +/// Record patterns in PureScript can mention different subsets of the full row type. +/// For example, `{ a }` and `{ a, b }` both match `{ a :: Int, b :: Int }`, but they +/// produce record constructors with different arities. The specialisation algorithm +/// requires all rows to have the same width, so we normalise to a canonical label set +/// before specialising. +/// +/// For non-record constructors this function returns the constructor unchanged. +fn canonicalise_record_constructor( + state: &CheckState, + constructor: PatternConstructor, + matrix: &PatternMatrix, +) -> PatternConstructor { + let PatternConstructor::Record { labels, fields } = &constructor else { + return constructor; + }; + + let mut canonical = { + let labels = labels.iter().cloned(); + let fields = fields.iter().copied(); + labels.zip(fields).collect_vec() + }; + + let initial_length = canonical.len(); + + for row in matrix { + let Some(&first) = row.first() else { + continue; + }; + + let pattern = state.patterns[first].clone(); + if let PatternKind::Constructor { + constructor: PatternConstructor::Record { labels, fields }, + } = pattern.kind + { + for (label, field) in iter::zip(labels, fields) { + if !canonical.iter().any(|(existing, _)| existing == &label) { + canonical.push((label, field)); + } + } + } + } + + if canonical.len() == initial_length { + return constructor; + } + + // Subtle: stable sorting by label + canonical.sort_by(|(a, _), (b, _)| a.cmp(b)); + + let (labels, fields) = canonical.into_iter().unzip(); + PatternConstructor::Record { labels, fields } +} + +/// Specialises a [`PatternMatrix`] given a [`PatternConstructor`]. +/// +/// See documentation below for [`specialise_vector`]. +fn specialise_matrix( + state: &mut CheckState, + expected: &PatternConstructor, + matrix: &PatternMatrix, +) -> PatternMatrix { + matrix.iter().filter_map(|row| specialise_vector(state, expected, row)).collect() +} + +/// Specialises a [`PatternVector`] given a [`PatternConstructor`]. +/// +/// Specialisation takes a pattern vector and applies the following rules: +/// 1. If the first column is a wildcard, it expands it to `n` wildcards +/// where `n` is the arity of the expected [`PatternConstructor`]. +/// For non-ADT constructors, arity is 0 (no expansion needed). +/// 2. It returns `None` for constructors that are not the expected +/// [`PatternConstructor`], which excludes them from the specialised matrix. +/// For example, a pattern vector specialised on `Just` removes `Nothing`. +/// 3. For matching constructors, it 'splats' the fields, effectively turning +/// a pattern vector like `[Just _]` into `[_]` or `[Nothing]` into `[]`. +/// For non-ADT constructors, arity is 0 so nothing is splatted. +fn specialise_vector( + state: &mut CheckState, + expected: &PatternConstructor, + vector: &PatternVector, +) -> Option { + let [first_column_id, ref tail_columns @ ..] = vector[..] else { + unreachable!("invariant violated: specialise_vector processed empty row"); + }; + + // Clone to release any borrow on state.patterns, allowing mutable + // access later when allocating wildcard patterns for record padding. + let first_pattern = state.patterns[first_column_id].clone(); + + if let PatternKind::Wildcard = first_pattern.kind { + // Expand wildcard to the expected constructor's arity + match expected { + PatternConstructor::DataConstructor { fields, .. } + | PatternConstructor::Record { fields, .. } => { + let wildcards = fields.iter().map(|&pattern_id| { + let t = state.patterns[pattern_id].t; + state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } + PatternConstructor::Array { fields } => { + let wildcards = fields.iter().map(|&pattern_id| { + let t = state.patterns[pattern_id].t; + state.allocate_wildcard(t) + }); + let tail_columns = tail_columns.iter().copied(); + return Some(iter::chain(wildcards, tail_columns).collect()); + } + _ => { + return Some(tail_columns.to_vec()); + } + } + } + + let PatternKind::Constructor { constructor } = first_pattern.kind else { + return Some(tail_columns.to_vec()); + }; + + // Check if constructors match + if !constructor.matches(expected) { + return None; + } + + // Splat fields for constructors with arity + match &constructor { + PatternConstructor::DataConstructor { fields, .. } => { + Some(fields.iter().copied().chain(tail_columns.iter().copied()).collect()) + } + PatternConstructor::Record { labels: actual_labels, fields: actual_fields } => { + specialise_record_fields(state, expected, actual_labels, actual_fields, tail_columns) + } + PatternConstructor::Array { fields } => { + Some(fields.iter().copied().chain(tail_columns.iter().copied()).collect()) + } + _ => Some(tail_columns.to_vec()), + } +} + +/// Maps the fields of an actual record pattern to the expected (canonical) label set. +/// +/// When record patterns in different branches mention different subsets of labels, +/// the actual pattern may have fewer labels than the expected canonical constructor. +/// This function aligns the actual fields to the expected label positions, inserting +/// wildcard patterns for any labels present in the expected set but absent from the +/// actual pattern. +fn specialise_record_fields( + state: &mut CheckState, + expected: &PatternConstructor, + actual_labels: &[SmolStr], + actual_fields: &[PatternId], + tail_columns: &[PatternId], +) -> Option { + let PatternConstructor::Record { labels: expected_labels, fields: expected_fields } = expected + else { + return Some( + iter::chain(actual_fields.iter().copied(), tail_columns.iter().copied()).collect(), + ); + }; + + // Fast path: labels match exactly. + if actual_labels == expected_labels.as_slice() { + return Some( + iter::chain(actual_fields.iter().copied(), tail_columns.iter().copied()).collect(), + ); + } + + let mut mapped_fields = Vec::with_capacity(expected_labels.len()); + for (expected_label, &expected_field) in expected_labels.iter().zip(expected_fields.iter()) { + if let Some(position) = actual_labels.iter().position(|label| label == expected_label) { + mapped_fields.push(actual_fields[position]); + } else { + let t = state.patterns[expected_field].t; + mapped_fields.push(state.allocate_wildcard(t)); + } + } + + Some(mapped_fields.into_iter().chain(tail_columns.iter().copied()).collect()) +} + +fn default_matrix(state: &CheckState, matrix: &PatternMatrix) -> PatternMatrix { + let filter_map = matrix.iter().filter_map(|row| { + let [first_column, ref default_columns @ ..] = row[..] else { + unreachable!("invariant violated: default_matrix processed empty row"); + }; + if let PatternKind::Wildcard = state.patterns[first_column].kind { + Some(default_columns.to_vec()) + } else { + None + } + }); + filter_map.collect() +} + +/// Key for identifying a unique constructor. +#[derive(Clone, PartialEq, Eq, Hash)] +enum ConstructorKey { + Data(FileId, TermItemId), + Record, + Array(usize), + Boolean(bool), + Integer(i32), + Number(bool, SmolStr), + String(SmolStr), + Char(char), +} + +impl ConstructorKey { + fn from_pattern_constructor(pc: &PatternConstructor) -> Self { + match pc { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + ConstructorKey::Data(*file_id, *item_id) + } + // All record constructors share the same key (single constructor semantics) + PatternConstructor::Record { .. } => ConstructorKey::Record, + // Array constructors are keyed by their length + PatternConstructor::Array { fields } => ConstructorKey::Array(fields.len()), + PatternConstructor::Boolean(b) => ConstructorKey::Boolean(*b), + PatternConstructor::Integer(i) => ConstructorKey::Integer(*i), + PatternConstructor::Number(negative, n) => { + let n = SmolStr::clone(n); + ConstructorKey::Number(*negative, n) + } + PatternConstructor::String(s) => { + let s = SmolStr::clone(s); + ConstructorKey::String(s) + } + PatternConstructor::Char(c) => ConstructorKey::Char(*c), + } + } +} + +#[derive(Clone, Debug)] +struct Sigma { + constructors: Vec, + missing: Vec, +} + +#[derive(Clone, Debug)] +enum MissingConstructor { + DataConstructor { file_id: FileId, item_id: TermItemId, fields: Vec }, + Boolean(bool), +} + +/// Extracts the sigma, a set of constructors from the first column of the matrix. +/// +/// Returns a list of unique constructors seen in the first column, keeping one +/// representative [`PatternConstructor`] per distinct constructor. Other patterns +/// like wildcards, records, and arrays are ignored for now. +fn collect_sigma( + state: &mut CheckState, + context: &CheckContext, + matrix: &PatternMatrix, + scrutinee_type: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut seen = FxHashSet::default(); + let mut constructors = vec![]; + + for row in matrix { + let [first_column, ..] = row[..] else { + continue; + }; + let pattern = state.patterns[first_column].clone(); + if let PatternKind::Constructor { constructor } = pattern.kind { + let key = ConstructorKey::from_pattern_constructor(&constructor); + if seen.insert(key) { + constructors.push(constructor); + } + } + } + + // Canonicalise record constructors to include all labels from the matrix. + // Different record patterns may mention different subsets of the row type, + // and the specialisation algorithm requires a consistent arity. + if let Some(index) = + constructors.iter().position(|c| matches!(c, PatternConstructor::Record { .. })) + { + let record = constructors.remove(index); + let canonical = canonicalise_record_constructor(state, record, matrix); + constructors.insert(index, canonical); + } + + let missing = collect_missing_constructors(state, context, scrutinee_type, &constructors)?; + Ok(Sigma { constructors, missing }) +} + +/// Checks whether the set of constructors (sigma) is complete for the scrutinee type. +/// +/// A sigma is complete if it contains all constructors of the data type. +/// For Boolean, both true and false must be present. +/// For records, there is exactly one constructor, so any record pattern makes sigma complete. +/// For other literal constructors (Integer, Number, String, Char), sigma is never complete +/// (infinite domains). +/// If we can't determine the type or its constructors, we conservatively return false. +fn sigma_is_complete(context: &CheckContext, sigma: &Sigma) -> QueryResult +where + Q: ExternalQueries, +{ + // Empty sigma is never complete + let Some(first) = sigma.constructors.first() else { + return Ok(false); + }; + + match first { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + // Get the indexed module for the constructor's file + let indexed = context.queries.indexed(*file_id)?; + + // Find the type this constructor belongs to + let Some(type_item_id) = indexed.pairs.constructor_type(*item_id) else { + return Ok(false); + }; + + // Get all constructors for this type + let all_constructors: FxHashSet = + indexed.pairs.data_constructors(type_item_id).collect(); + + // Check if sigma covers all constructors + let sigma_terms: FxHashSet = sigma + .constructors + .iter() + .filter_map(|c| match c { + PatternConstructor::DataConstructor { item_id, .. } => Some(*item_id), + _ => None, + }) + .collect(); + + Ok(all_constructors.iter().all(|term_id| sigma_terms.contains(term_id))) + } + // Records have exactly one constructor, so sigma is always complete for records + PatternConstructor::Record { .. } => Ok(true), + // Arrays have infinite possible lengths, so sigma is never complete + PatternConstructor::Array { .. } => Ok(false), + PatternConstructor::Boolean(_) => { + // Boolean is complete when both true and false are present + let has_true = + sigma.constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(true))); + let has_false = + sigma.constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(false))); + Ok(has_true && has_false) + } + // Other literal constructors have infinite domains, so they're never complete + PatternConstructor::Integer(_) + | PatternConstructor::Number(_, _) + | PatternConstructor::String(_) + | PatternConstructor::Char(_) => Ok(false), + } +} + +fn collect_missing_constructors( + state: &mut CheckState, + context: &CheckContext, + scrutinee_type: TypeId, + constructors: &[PatternConstructor], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(first_constructor) = constructors.first() else { + return Ok(vec![]); + }; + + match first_constructor { + PatternConstructor::DataConstructor { file_id, item_id, .. } => { + let indexed = context.queries.indexed(*file_id)?; + + let Some(type_item_id) = indexed.pairs.constructor_type(*item_id) else { + return Ok(vec![]); + }; + + let sigma: FxHashSet = constructors + .iter() + .filter_map(|c| match c { + PatternConstructor::DataConstructor { item_id, .. } => Some(*item_id), + _ => None, + }) + .collect(); + let arguments = extract_all_applications(state, context, scrutinee_type)?; + + let mut missing = vec![]; + for missing_item_id in indexed.pairs.data_constructors(type_item_id) { + if !sigma.contains(&missing_item_id) { + let fields = constructor_field_types( + state, + context, + *file_id, + missing_item_id, + &arguments, + )?; + missing.push(MissingConstructor::DataConstructor { + file_id: *file_id, + item_id: missing_item_id, + fields, + }); + } + } + + Ok(missing) + } + // Arrays have infinite possible lengths, so we don't report specific missing values. + // The algorithm will fall back to wildcard suggestion. + PatternConstructor::Array { .. } => Ok(vec![]), + PatternConstructor::Boolean(_) => { + // Check which boolean values are missing + let has_true = + constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(true))); + let has_false = + constructors.iter().any(|c| matches!(c, PatternConstructor::Boolean(false))); + let mut missing = vec![]; + if !has_true { + missing.push(MissingConstructor::Boolean(true)); + } + if !has_false { + missing.push(MissingConstructor::Boolean(false)); + } + Ok(missing) + } + // Other literal constructors have infinite domains, so we don't report specific missing values + _ => Ok(vec![]), + } +} + +fn constructor_field_types( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let constructor_type = if file_id == context.id { + state.checked.lookup_term(term_id) + } else { + let checked = context.queries.checked2(file_id)?; + checked.lookup_term(term_id) + }; + + if let Some(constructor_type) = constructor_type { + let constructor = instantiate_with_arguments(state, context, constructor_type, arguments)?; + let toolkit::InspectFunction { arguments: fields, .. } = + toolkit::inspect_function(state, context, constructor)?; + Ok(fields) + } else { + let arity = get_constructor_arity(context, file_id, term_id)?; + let unknown = context.unknown("missing constructor field"); + Ok(iter::repeat_n(unknown, arity).collect()) + } +} + +/// Extracts type and kind arguments from a type application. +/// +/// Peels off [`Type::Application`] and [`Type::KindApplication`] layers, +/// collecting both type and kind application arguments. +fn extract_all_applications( + state: &mut CheckState, + context: &CheckContext, + applied_type: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + let mut current_id = applied_type; + + safe_loop! { + current_id = normalise::normalise(state, context, current_id)?; + match context.lookup_type(current_id) { + Type::Application(function, argument) => { + arguments.push(argument); + current_id = function; + } + Type::KindApplication(function, argument) => { + arguments.push(argument); + current_id = function; + } + _ => break, + } + } + + arguments.reverse(); + Ok(arguments) +} + +/// Instantiates [`Type::Forall`] with the provided arguments. +/// +/// This function falls back to constructing rigid variables if there's +/// not enough arguments provided. +fn instantiate_with_arguments( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, + arguments: impl AsRef<[TypeId]>, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut arguments_iter = arguments.as_ref().iter().copied(); + + safe_loop! { + type_id = normalise::normalise(state, context, type_id)?; + match context.lookup_type(type_id) { + Type::Forall(binder_id, inner) => { + let binder = context.lookup_forall_binder(binder_id); + let argument_type = if let Some(argument) = arguments_iter.next() { + argument + } else { + context.intern_rigid(binder.name, state.depth, binder.kind) + }; + type_id = SubstituteName::one(state, context, binder.name, argument_type, inner)?; + } + _ => break, + } + } + + Ok(type_id) +} + +/// Gets the arity (number of fields) of a constructor. +fn get_constructor_arity( + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let on_lowered = |lowered: &lowering::LoweredModule| { + if let Some(lowering::TermItemIr::Constructor { arguments }) = + lowered.info.get_term_item(term_id) + { + arguments.len() + } else { + 0 + } + }; + if file_id == context.id { + let lowered = &context.lowered; + Ok(on_lowered(lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(on_lowered(&lowered)) + } +} + +pub struct ExhaustivenessReport { + pub missing: Option>, + pub redundant: Vec, +} + +pub fn check_lambda_patterns( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + binders: &[lowering::BinderId], +) -> QueryResult +where + Q: ExternalQueries, +{ + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); + } + + let unconditional = + collect_unconditional_rows(state, context, &[binders], pattern_types, |binders| { + (binders, &None) + })?; + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + +pub fn check_case_patterns( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + branches: &[lowering::CaseBranch], +) -> QueryResult +where + Q: ExternalQueries, +{ + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); + } + + let unconditional = collect_unconditional_rows( + state, + context, + branches, + pattern_types, + |branch: &lowering::CaseBranch| (&branch.binders, &branch.guarded_expression), + )?; + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + +pub fn check_equation_patterns( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + equations: &[lowering::Equation], +) -> QueryResult +where + Q: ExternalQueries, +{ + if pattern_types.is_empty() { + return Ok(ExhaustivenessReport { missing: None, redundant: vec![] }); + } + + let unconditional = collect_unconditional_rows( + state, + context, + equations, + pattern_types, + |equation: &lowering::Equation| (&equation.binders, &equation.guarded), + )?; + + check_exhaustiveness_core(state, context, pattern_types, unconditional) +} + +/// Returns `true` if any alternative in a conditional guard set is trivially true. +fn has_trivially_true_alternative( + context: &CheckContext, + pattern_guarded: &[lowering::PatternGuarded], +) -> bool +where + Q: ExternalQueries, +{ + pattern_guarded.iter().any(|pg| { + !pg.pattern_guards.is_empty() + && pg.pattern_guards.iter().all(|g| is_trivially_true_guard(context, g)) + }) +} + +/// Returns `true` if the guard is `true` or `otherwise` from `Data.Boolean`. +fn is_trivially_true_guard(context: &CheckContext, guard: &lowering::PatternGuard) -> bool +where + Q: ExternalQueries, +{ + if guard.binder.is_some() { + return false; + } + let Some(expr_id) = guard.expression else { + return false; + }; + let Some(kind) = context.lowered.info.get_expression_kind(expr_id) else { + return false; + }; + match kind { + lowering::ExpressionKind::Boolean { boolean: true } => true, + lowering::ExpressionKind::Variable { + resolution: Some(lowering::TermVariableResolution::Reference(file_id, term_id)), + } => context.known_terms.otherwise == Some((*file_id, *term_id)), + _ => false, + } +} + +fn collect_unconditional_rows( + state: &mut CheckState, + context: &CheckContext, + items: &[T], + pattern_types: &[TypeId], + to_binders: F, +) -> QueryResult> +where + Q: ExternalQueries, + F: Fn(&T) -> (&[lowering::BinderId], &Option), +{ + let mut pattern_rows = vec![]; + for item in items { + let (binders, guarded) = to_binders(item); + + match guarded { + Some(lowering::GuardedExpression::Unconditional { .. }) | None => {} + Some(lowering::GuardedExpression::Conditionals { pattern_guarded }) => { + if !has_trivially_true_alternative(context, pattern_guarded) { + continue; + } + } + } + + let mut pattern_row = vec![]; + for &binder_id in binders { + pattern_row.push(convert::convert_binder(state, context, binder_id)?); + } + + let additional = pattern_types.iter().skip(pattern_row.len()); + pattern_row.extend(additional.map(|&t| state.allocate_wildcard(t))); + + if !pattern_row.is_empty() { + pattern_rows.push(pattern_row); + } + } + Ok(pattern_rows) +} + +fn check_exhaustiveness_core( + state: &mut CheckState, + context: &CheckContext, + pattern_types: &[TypeId], + unconditional: PatternMatrix, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut redundant = vec![]; + let mut matrix = vec![]; + for vector in &unconditional { + let useful = algorithm_u(state, context, &matrix, vector)?; + if useful { + matrix.push(PatternVector::clone(vector)); + } else { + redundant.push(pretty::pretty_witness(context, state, vector)); + } + } + + let query = pattern_types.iter().map(|&t| state.allocate_wildcard(t)).collect(); + let witnesses = algorithm_m(state, context, &unconditional, &query)?; + let missing = witnesses.map(|witnesses| { + witnesses + .iter() + .take(5) + .map(|witness| pretty::pretty_witness(context, state, witness)) + .collect() + }); + + Ok(ExhaustivenessReport { missing, redundant }) +} diff --git a/compiler-core/checking2/src/core/exhaustive/convert.rs b/compiler-core/checking2/src/core/exhaustive/convert.rs new file mode 100644 index 00000000..34684542 --- /dev/null +++ b/compiler-core/checking2/src/core/exhaustive/convert.rs @@ -0,0 +1,382 @@ +use std::sync::Arc; + +use building_types::QueryResult; +use files::FileId; +use indexing::TermItemId; +use itertools::Itertools; +use lowering::{BinderId, TermOperatorId}; +use rustc_hash::FxHashMap; +use smol_str::SmolStr; +use sugar::OperatorTree; + +use crate::context::CheckContext; +use crate::core::exhaustive::{PatternConstructor, PatternId}; +use crate::core::{Type, TypeId, normalise}; +use crate::state::CheckState; +use crate::{ExternalQueries, OperatorBranchTypes, safe_loop}; + +pub fn convert_binder( + state: &mut CheckState, + context: &CheckContext, + id: BinderId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t = state + .checked + .nodes + .lookup_binder(id) + .unwrap_or_else(|| context.unknown("unresolved binder")); + + let Some(kind) = context.lowered.info.get_binder_kind(id) else { + return Ok(state.allocate_wildcard(t)); + }; + + match kind { + lowering::BinderKind::Typed { binder, .. } => match binder { + Some(id) => convert_binder(state, context, *id), + None => Ok(state.allocate_wildcard(t)), + }, + lowering::BinderKind::OperatorChain { .. } => { + convert_operator_chain_binder(state, context, id, t) + } + lowering::BinderKind::Integer { value } => match value { + Some(v) => { + let constructor = PatternConstructor::Integer(*v); + Ok(state.allocate_constructor(constructor, t)) + } + None => Ok(state.allocate_wildcard(t)), + }, + lowering::BinderKind::Number { negative, value } => { + if let Some(value) = value { + let constructor = PatternConstructor::Number(*negative, SmolStr::clone(value)); + Ok(state.allocate_constructor(constructor, t)) + } else { + Ok(state.allocate_wildcard(t)) + } + } + lowering::BinderKind::Constructor { resolution, arguments } => { + convert_constructor_binder(state, context, &resolution, &arguments, t) + } + lowering::BinderKind::Variable { .. } => Ok(state.allocate_wildcard(t)), + lowering::BinderKind::Named { binder, .. } => match binder { + Some(id) => convert_binder(state, context, *id), + None => Ok(state.allocate_wildcard(t)), + }, + lowering::BinderKind::Wildcard => Ok(state.allocate_wildcard(t)), + lowering::BinderKind::String { value, .. } => { + if let Some(value) = value { + let constructor = PatternConstructor::String(SmolStr::clone(value)); + Ok(state.allocate_constructor(constructor, t)) + } else { + Ok(state.allocate_wildcard(t)) + } + } + lowering::BinderKind::Char { value } => match value { + Some(v) => { + let constructor = PatternConstructor::Char(*v); + Ok(state.allocate_constructor(constructor, t)) + } + None => Ok(state.allocate_wildcard(t)), + }, + lowering::BinderKind::Boolean { boolean } => { + let constructor = PatternConstructor::Boolean(*boolean); + Ok(state.allocate_constructor(constructor, t)) + } + lowering::BinderKind::Array { array } => lower_array_binder(state, context, &array, t), + lowering::BinderKind::Record { record } => lower_record_binder(state, context, &record, t), + lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { + Some(id) => convert_binder(state, context, *id), + None => Ok(state.allocate_wildcard(t)), + }, + } +} + +fn lower_array_binder( + state: &mut CheckState, + context: &CheckContext, + array: &[BinderId], + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut fields = vec![]; + for &element in array { + fields.push(convert_binder(state, context, element)?); + } + let constructor = PatternConstructor::Array { fields }; + Ok(state.allocate_constructor(constructor, t)) +} + +fn lower_record_binder( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::BinderRecordItem], + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + match try_build_record_constructor(state, context, record, t)? { + Some((labels, fields)) => { + let constructor = PatternConstructor::Record { labels, fields }; + Ok(state.allocate_constructor(constructor, t)) + } + None => { + // Fallback: use a wildcard when we can't build a canonical record constructor + Ok(state.allocate_wildcard(t)) + } + } +} + +fn try_build_record_constructor( + state: &mut CheckState, + context: &CheckContext, + record: &[lowering::BinderRecordItem], + t: TypeId, +) -> QueryResult, Vec)>> +where + Q: ExternalQueries, +{ + let expanded_t = normalise_expand_type(state, context, t)?; + + let (constructor, arguments) = extract_type_application(state, context, expanded_t)?; + + if constructor != context.prim.record { + return Ok(None); + } + + let Some(row_type_id) = arguments.first() else { + return Ok(None); + }; + + let row_type_id = normalise::normalise(state, context, *row_type_id)?; + + let row_fields = if let Type::Row(row_type_id) = context.lookup_type(row_type_id) { + context.lookup_row_type(row_type_id).fields + } else { + return Ok(None); + }; + + let field_type_map: FxHashMap = + row_fields.iter().map(|field| (SmolStr::clone(&field.label), field.id)).collect(); + + let mut provided_patterns = FxHashMap::default(); + for element in record.iter() { + match element { + lowering::BinderRecordItem::RecordField { name, value } => { + let Some(name) = name.as_ref().map(SmolStr::clone) else { + continue; + }; + + let pattern = if let Some(value) = value { + convert_binder(state, context, *value)? + } else { + state.allocate_wildcard(context.unknown("record field")) + }; + + provided_patterns.insert(name, pattern); + } + lowering::BinderRecordItem::RecordPun { id: _, name } => { + let Some(name) = name.as_ref().map(SmolStr::clone) else { + continue; + }; + + let t = field_type_map + .get(&name) + .copied() + .unwrap_or_else(|| context.unknown("record pun")); + + let pattern = state.allocate_wildcard(t); + provided_patterns.insert(name, pattern); + } + } + } + + let mut sorted_labels = field_type_map.keys().cloned().collect_vec(); + sorted_labels.sort(); + + let mut labels = Vec::with_capacity(sorted_labels.len()); + let mut fields = Vec::with_capacity(sorted_labels.len()); + + for label in sorted_labels { + let pattern = provided_patterns.get(&label).copied().unwrap_or_else(|| { + let t = field_type_map[&label]; + state.allocate_wildcard(t) + }); + labels.push(label); + fields.push(pattern); + } + + Ok(Some((labels, fields))) +} + +fn convert_constructor_binder( + state: &mut CheckState, + context: &CheckContext, + resolution: &Option<(FileId, TermItemId)>, + arguments: &Arc<[BinderId]>, + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some((file_id, item_id)) = *resolution else { + return Ok(state.allocate_wildcard(t)); + }; + + let mut fields = vec![]; + for &argument in arguments.iter() { + fields.push(convert_binder(state, context, argument)?); + } + + let constructor = PatternConstructor::DataConstructor { file_id, item_id, fields }; + Ok(state.allocate_constructor(constructor, t)) +} + +fn convert_operator_chain_binder( + state: &mut CheckState, + context: &CheckContext, + id: BinderId, + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(tree) = context.bracketed.binders.get(&id) else { + return Ok(state.allocate_wildcard(t)); + }; + + let Ok(tree) = tree else { + return Ok(state.allocate_wildcard(t)); + }; + + convert_operator_tree(state, context, tree, t) +} + +fn convert_operator_tree( + state: &mut CheckState, + context: &CheckContext, + tree: &OperatorTree, + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + match tree { + OperatorTree::Leaf(None) => Ok(state.allocate_wildcard(t)), + OperatorTree::Leaf(Some(binder_id)) => convert_binder(state, context, *binder_id), + OperatorTree::Branch(operator_id, children) => { + convert_operator_branch(state, context, *operator_id, children, t) + } + } +} + +fn convert_operator_branch( + state: &mut CheckState, + context: &CheckContext, + operator_id: TermOperatorId, + children: &[OperatorTree; 2], + t: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some((file_id, item_id)) = context.lowered.info.get_term_operator(operator_id) else { + return Ok(state.allocate_wildcard(t)); + }; + + // The operator_id points to itself, thus we need to follow the + // resolution to find the constructor that it actually points to. + let Some((constructor_file_id, constructor_item_id)) = + resolve_term_operator(context, file_id, item_id)? + else { + return Ok(state.allocate_wildcard(t)); + }; + + let Some(OperatorBranchTypes { left, right, result }) = + state.checked.nodes.lookup_term_operator(operator_id) + else { + return Ok(state.allocate_wildcard(t)); + }; + + let [left_tree, right_tree] = children; + + let left_pattern = convert_operator_tree(state, context, left_tree, left)?; + let right_pattern = convert_operator_tree(state, context, right_tree, right)?; + + let constructor = PatternConstructor::DataConstructor { + file_id: constructor_file_id, + item_id: constructor_item_id, + fields: vec![left_pattern, right_pattern], + }; + + Ok(state.allocate_constructor(constructor, result)) +} + +fn resolve_term_operator( + context: &CheckContext, + file_id: FileId, + item_id: TermItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let on_lowered = |lowered: &lowering::LoweredModule| { + if let Some(lowering::TermItemIr::Operator { resolution, .. }) = + lowered.info.get_term_item(item_id) + { + *resolution + } else { + None + } + }; + + if file_id == context.id { + Ok(on_lowered(&context.lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(on_lowered(&lowered)) + } +} + +fn extract_type_application( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, +) -> QueryResult<(TypeId, Vec)> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + + safe_loop! { + type_id = normalise::normalise(state, context, type_id)?; + match context.lookup_type(type_id) { + Type::Application(function, argument) => { + arguments.push(argument); + type_id = function; + } + Type::KindApplication(function, _) => { + type_id = function; + } + _ => break, + } + } + + arguments.reverse(); + Ok((type_id, arguments)) +} + +fn normalise_expand_type( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + normalise::normalise(state, context, type_id) +} diff --git a/compiler-core/checking2/src/core/exhaustive/pretty.rs b/compiler-core/checking2/src/core/exhaustive/pretty.rs new file mode 100644 index 00000000..424c4300 --- /dev/null +++ b/compiler-core/checking2/src/core/exhaustive/pretty.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use files::FileId; +use indexing::TermItemId; +use smol_str::{SmolStr, SmolStrBuilder}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::exhaustive::{PatternConstructor, PatternId, PatternKind, WitnessVector}; +use crate::state::CheckState; + +pub fn pretty_witness( + context: &CheckContext, + state: &CheckState, + witness: &WitnessVector, +) -> SmolStr +where + Q: ExternalQueries, +{ + join_smolstr(witness.iter().map(|&id| pretty_pattern(context, state, id)), ", ") +} + +fn pretty_pattern(context: &CheckContext, state: &CheckState, id: PatternId) -> SmolStr +where + Q: ExternalQueries, +{ + let pattern = &state.patterns[id]; + match &pattern.kind { + PatternKind::Wildcard => SmolStr::new_inline("_"), + PatternKind::Constructor { constructor } => pretty_constructor(context, state, constructor), + } +} + +fn join_smolstr(iterator: impl Iterator, separator: &str) -> SmolStr { + let mut builder = SmolStrBuilder::default(); + join_with_sep(&mut builder, iterator, separator, |builder, item| builder.push_str(&item)); + builder.finish() +} + +fn join_with_sep( + builder: &mut SmolStrBuilder, + iter: impl Iterator, + sep: &str, + mut render: impl FnMut(&mut SmolStrBuilder, T), +) { + let mut first = true; + for item in iter { + if !first { + builder.push_str(sep); + } + first = false; + render(builder, item); + } +} + +fn pretty_constructor( + context: &CheckContext, + state: &CheckState, + constructor: &PatternConstructor, +) -> SmolStr +where + Q: ExternalQueries, +{ + match constructor { + PatternConstructor::DataConstructor { file_id, item_id, fields } => { + let name = lookup_constructor_name(context, *file_id, *item_id) + .unwrap_or_else(|| SmolStr::new_inline("")); + + if fields.is_empty() { + return name; + } + + let mut builder = SmolStrBuilder::default(); + builder.push_str(&name); + + for &id in fields.iter() { + builder.push(' '); + let rendered = pretty_pattern(context, state, id); + let pattern = &state.patterns[id]; + if let PatternKind::Constructor { constructor } = &pattern.kind + && !constructor.fields().is_empty() + { + builder.push('('); + builder.push_str(&rendered); + builder.push(')'); + } else { + builder.push_str(&rendered); + } + } + + builder.finish() + } + PatternConstructor::Record { labels, fields } => { + if labels.len() != fields.len() { + return SmolStr::new_inline("{ }"); + } + + let mut builder = SmolStrBuilder::default(); + builder.push_str("{ "); + join_with_sep( + &mut builder, + labels.iter().zip(fields.iter()), + ", ", + |b: &mut SmolStrBuilder, (label, field_id)| { + let field = pretty_pattern(context, state, *field_id); + b.push_str(label); + b.push_str(": "); + b.push_str(&field); + }, + ); + builder.push_str(" }"); + builder.finish() + } + PatternConstructor::Array { fields } => { + let mut builder = SmolStrBuilder::default(); + builder.push('['); + join_with_sep( + &mut builder, + fields.iter().copied(), + ", ", + |b: &mut SmolStrBuilder, id| { + let rendered = pretty_pattern(context, state, id); + b.push_str(&rendered); + }, + ); + builder.push(']'); + builder.finish() + } + PatternConstructor::Boolean(b) => SmolStr::from(b.to_string()), + PatternConstructor::Char(c) => { + let mut builder = SmolStrBuilder::default(); + builder.push('\''); + builder.push(*c); + builder.push('\''); + builder.finish() + } + PatternConstructor::String(s) => { + let mut builder = SmolStrBuilder::default(); + builder.push('"'); + builder.push_str(s); + builder.push('"'); + builder.finish() + } + PatternConstructor::Integer(i) => SmolStr::from(i.to_string()), + PatternConstructor::Number(negative, n) => { + let mut builder = SmolStrBuilder::default(); + if *negative { + builder.push('-'); + } + builder.push_str(n.as_ref()); + builder.finish() + } + } +} + +fn lookup_constructor_name( + context: &CheckContext, + file_id: FileId, + term_id: TermItemId, +) -> Option +where + Q: ExternalQueries, +{ + let indexed = if file_id == context.id { + Arc::clone(&context.indexed) + } else { + context.queries.indexed(file_id).ok()? + }; + + let item = &indexed.items[term_id]; + item.name.clone() +} diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs index af8a3c8c..e2ad28c7 100644 --- a/compiler-core/checking2/src/source/binder.rs +++ b/compiler-core/checking2/src/source/binder.rs @@ -11,7 +11,7 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{RowField, RowType, Type, TypeId, normalise, toolkit, unification}; use crate::error::{ErrorCrumb, ErrorKind}; -use crate::source::types; +use crate::source::{operator, types}; use crate::state::CheckState; #[derive(Copy, Clone, Debug)] @@ -131,8 +131,7 @@ where } lowering::BinderKind::OperatorChain { .. } => { - // TODO(operators): implement IsOperator for BinderId - let inferred_type = context.unknown("operator chain binder"); + let (_, inferred_type) = operator::infer_operator_chain(state, context, binder_id)?; if let BinderMode::Check { expected_type, elaborating } = mode { subtype_for_mode(state, context, inferred_type, expected_type, elaborating)?; diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 85a4c0d4..46e30ad3 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -7,11 +7,11 @@ use lowering::IsElement; use sugar::OperatorTree; use sugar::bracketing::BracketingResult; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{TypeId, normalise, toolkit, unification}; -use crate::source::{terms, types}; +use crate::source::{binder, terms, types}; use crate::state::CheckState; +use crate::{ExternalQueries, OperatorBranchTypes}; #[derive(Copy, Clone, Debug)] enum OperatorKindMode { @@ -64,6 +64,10 @@ where OperatorTree::Leaf(Some(type_id)) => match mode { OperatorKindMode::Infer => E::infer_surface(state, context, *type_id), OperatorKindMode::Check { expected_type } => { + // Peel constraints from the expected type as givens, + // so operator arguments like `unsafePartial $ expr` + // can discharge constraints like Partial properly. + let expected_type = toolkit::collect_givens(state, context, expected_type)?; E::check_surface(state, context, *type_id, expected_type) } }, @@ -85,6 +89,7 @@ where traverse_operator_branch( state, context, + *operator_id, (file_id, item_id), operator_type, children, @@ -97,6 +102,7 @@ where fn traverse_operator_branch( state: &mut CheckState, context: &CheckContext, + operator_id: E::OperatorId, operator: (FileId, E::ItemId), operator_type: TypeId, children: &[OperatorTree; 2], @@ -113,6 +119,8 @@ where }; let operator_type = toolkit::instantiate_unifications(state, context, operator_type)?; + let operator_type = toolkit::collect_wanteds(state, context, operator_type)?; + let Some((left_type, operator_type)) = toolkit::decompose_function(state, context, operator_type)? else { @@ -126,7 +134,12 @@ where return Ok(unknown); }; + E::record_branch_types(state, operator_id, left_type, right_type, result_type); + if let OperatorKindMode::Check { expected_type } = mode { + // Peel constraints from the expected type as givens, + // so operator result constraints can be discharged. + let expected_type = toolkit::collect_givens(state, context, expected_type)?; let _ = unification::subtype(state, context, result_type, expected_type)?; } @@ -212,6 +225,14 @@ pub trait IsOperator: IsElement { result_tree: (Self::Elaborated, Self::Elaborated), result_type: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)>; + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ); } impl IsOperator for lowering::ExpressionId { @@ -263,15 +284,28 @@ impl IsOperator for lowering::ExpressionId { } fn build( - state: &mut CheckState, + _state: &mut CheckState, _context: &CheckContext, (_, _): (FileId, Self::ItemId), (_, _): (Self::Elaborated, Self::Elaborated), result_type: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)> { - let _ = state; Ok(((), result_type)) } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .checked + .nodes + .term_operator + .insert(operator_id, OperatorBranchTypes { left, right, result }); + } } impl IsOperator for lowering::TypeId { @@ -333,4 +367,91 @@ impl IsOperator for lowering::TypeId { let result_kind = normalise::normalise(state, context, result_kind)?; Ok((elaborated_type, result_kind)) } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .checked + .nodes + .type_operator + .insert(operator_id, OperatorBranchTypes { left, right, result }); + } +} + +impl IsOperator for lowering::BinderId { + type ItemId = TermItemId; + type Elaborated = (); + + fn unknown_elaborated(_context: &CheckContext) -> Self::Elaborated {} + + fn lookup_tree<'q>( + context: &'q CheckContext, + id: Self, + ) -> Option<&'q BracketingResult> { + context.bracketed.binders.get(&id) + } + + fn lookup_operator( + context: &CheckContext, + id: Self::OperatorId, + ) -> Option<(FileId, Self::ItemId)> { + context.lowered.info.get_term_operator(id) + } + + fn lookup_item( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: Self::ItemId, + ) -> QueryResult { + toolkit::lookup_file_term_operator(state, context, file_id, item_id) + } + + fn infer_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let inferred_type = binder::infer_binder(state, context, id)?; + Ok(((), inferred_type)) + } + + fn check_surface( + state: &mut CheckState, + context: &CheckContext, + id: Self, + expected: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + let checked_type = binder::check_binder(state, context, id, expected)?; + Ok(((), checked_type)) + } + + fn build( + _state: &mut CheckState, + _context: &CheckContext, + (_, _): (FileId, Self::ItemId), + (_, _): (Self::Elaborated, Self::Elaborated), + result_type: TypeId, + ) -> QueryResult<(Self::Elaborated, TypeId)> { + Ok(((), result_type)) + } + + fn record_branch_types( + state: &mut CheckState, + operator_id: Self::OperatorId, + left: TypeId, + right: TypeId, + result: TypeId, + ) { + state + .checked + .nodes + .term_operator + .insert(operator_id, OperatorBranchTypes { left, right, result }); + } } diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index faf6f077..0c03ff0b 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -4,7 +4,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, unification}; +use crate::core::{TypeId, exhaustive, toolkit, unification}; use crate::error::ErrorKind; use crate::source::terms::form_let; use crate::source::{binder, terms}; @@ -47,6 +47,13 @@ where } } + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, group_type)?; + + let exhaustiveness = + exhaustive::check_equation_patterns(state, context, &arguments, equations)?; + state.report_exhaustiveness(context, exhaustiveness); + Ok(()) } @@ -104,6 +111,9 @@ where } } + let exhaustiveness = exhaustive::check_equation_patterns(state, context, arguments, equations)?; + state.report_exhaustiveness(context, exhaustiveness); + Ok(()) } diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 92a36339..d7ab0d26 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -2,7 +2,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::toolkit; +use crate::core::{exhaustive, toolkit}; use crate::error::ErrorCrumb; use crate::source::terms::equations; use crate::source::{binder, types}; @@ -54,7 +54,17 @@ where expression_type }; - binder::check_binder(state, context, binder, expression_type)?; + let binder_type = binder::check_binder(state, context, binder, expression_type)?; + + let exhaustiveness = + exhaustive::check_lambda_patterns(state, context, &[binder_type], &[binder])?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(context, exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } Ok(()) } diff --git a/compiler-core/checking2/src/source/terms/forms.rs b/compiler-core/checking2/src/source/terms/forms.rs index 11d3d6d8..8711c90a 100644 --- a/compiler-core/checking2/src/source/terms/forms.rs +++ b/compiler-core/checking2/src/source/terms/forms.rs @@ -2,7 +2,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, toolkit, unification}; +use crate::core::{TypeId, exhaustive, toolkit, unification}; use crate::source::terms::{equations, form_let}; use crate::source::{binder, terms}; use crate::state::CheckState; @@ -84,7 +84,19 @@ where state.fresh_unification(context.queries, context.prim.t) }; - Ok(context.intern_function_chain(argument_types.iter().copied(), result_type)) + let function_type = context.intern_function_chain(argument_types.iter().copied(), result_type); + + let exhaustiveness = + exhaustive::check_lambda_patterns(state, context, &argument_types, binders)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(context, exhaustiveness); + + if has_missing { + Ok(context.intern_constrained(context.prim.partial, function_type)) + } else { + Ok(function_type) + } } pub fn check_lambda( @@ -119,7 +131,18 @@ where state.fresh_unification(context.queries, context.prim.t) }; - Ok(context.intern_function_chain(arguments.iter().copied(), result_type)) + let function_type = context.intern_function_chain(arguments.iter().copied(), result_type); + + let exhaustiveness = exhaustive::check_lambda_patterns(state, context, &arguments, binders)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(context, exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } + + Ok(function_type) } pub fn instantiate_trunk_types( @@ -172,7 +195,16 @@ where } } - Ok(result_type) + let exhaustiveness = exhaustive::check_case_patterns(state, context, &trunk_types, branches)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(context, exhaustiveness); + + if has_missing { + Ok(context.intern_constrained(context.prim.partial, result_type)) + } else { + Ok(result_type) + } } pub fn check_case_of( @@ -202,6 +234,15 @@ where } } + let exhaustiveness = exhaustive::check_case_patterns(state, context, &trunk_types, branches)?; + + let has_missing = exhaustiveness.missing.is_some(); + state.report_exhaustiveness(context, exhaustiveness); + + if has_missing { + state.push_wanted(context.prim.partial); + } + Ok(expected) } diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 6704859a..bab89f13 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -1,12 +1,16 @@ //! Implements the algorithm's core state structures. use std::mem; +use std::sync::Arc; use building_types::QueryResult; use files::FileId; use rustc_hash::FxHashMap; use crate::context::CheckContext; +use crate::core::exhaustive::{ + ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, +}; use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty, zonk}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; @@ -120,6 +124,7 @@ pub struct CheckState { pub names: Names, pub unifications: Unifications, pub implications: Implications, + pub patterns: PatternStorage, pub kind_scope: KindScope, pub defer_synonym_expansion: bool, pub depth: Depth, @@ -134,6 +139,7 @@ impl CheckState { names: Names::new(file_id), unifications: Default::default(), implications: Default::default(), + patterns: Default::default(), kind_scope: Default::default(), defer_synonym_expansion: Default::default(), depth: Depth(0), @@ -199,4 +205,43 @@ impl CheckState { let pretty = pretty::Pretty::new(context.queries).render(id); Ok(context.queries.intern_smol_str(pretty)) } + + pub fn report_exhaustiveness( + &mut self, + context: &CheckContext, + exhaustiveness: ExhaustivenessReport, + ) where + Q: ExternalQueries, + { + if let Some(patterns) = exhaustiveness.missing { + let patterns: Vec<_> = patterns + .into_iter() + .map(|pattern| context.queries.intern_smol_str(pattern)) + .collect(); + self.insert_error(ErrorKind::MissingPatterns { patterns: Arc::from(patterns) }); + } + + if !exhaustiveness.redundant.is_empty() { + let patterns = Arc::from(exhaustiveness.redundant); + self.insert_error(ErrorKind::RedundantPatterns { patterns }); + } + } + + pub fn allocate_pattern(&mut self, kind: PatternKind, t: TypeId) -> PatternId { + let pattern = Pattern { kind, t }; + self.patterns.intern(pattern) + } + + pub fn allocate_constructor( + &mut self, + constructor: PatternConstructor, + t: TypeId, + ) -> PatternId { + let kind = PatternKind::Constructor { constructor }; + self.allocate_pattern(kind, t) + } + + pub fn allocate_wildcard(&mut self, t: TypeId) -> PatternId { + self.allocate_pattern(PatternKind::Wildcard, t) + } } From 56d1e806a9ac3f5d870dff06c7967d058f505703 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 02:07:39 +0800 Subject: [PATCH 272/386] Add integration tests for exhaustiveness checking --- .../030_exhaustive_case_infer/Main.purs | 6 +++ .../030_exhaustive_case_infer/Main.snap | 32 +++++++++++++ .../031_exhaustive_case_redundant/Main.purs | 8 ++++ .../031_exhaustive_case_redundant/Main.snap | 32 +++++++++++++ .../Main.purs | 6 +++ .../Main.snap | 29 +++++++++++ .../033_exhaustive_guards_otherwise/Main.purs | 11 +++++ .../033_exhaustive_guards_otherwise/Main.snap | 48 +++++++++++++++++++ .../034_exhaustive_let_pattern/Main.purs | 9 ++++ .../034_exhaustive_let_pattern/Main.snap | 32 +++++++++++++ .../Main.purs | 8 ++++ .../Main.snap | 30 ++++++++++++ .../tests/checking2/generated.rs | 12 +++++ 13 files changed, 263 insertions(+) create mode 100644 tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap create mode 100644 tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.purs create mode 100644 tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.snap create mode 100644 tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.purs create mode 100644 tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.snap create mode 100644 tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.purs create mode 100644 tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap create mode 100644 tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.purs create mode 100644 tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap create mode 100644 tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.snap diff --git a/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.purs b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.purs new file mode 100644 index 00000000..95a569e2 --- /dev/null +++ b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.purs @@ -0,0 +1,6 @@ +module Main where + +data Maybe a = Just a | Nothing + +test = case _ of + Just _ -> 1 diff --git a/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap new file mode 100644 index 00000000..d03c6fb4 --- /dev/null +++ b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: forall (t1 :: Type). Maybe (t1 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + InferringExpression( + AstId(13), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.purs b/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.purs new file mode 100644 index 00000000..7c32a831 --- /dev/null +++ b/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.purs @@ -0,0 +1,8 @@ +module Main where + +data Maybe a = Just a | Nothing + +test = case _ of + Just _ -> 1 + Just _ -> 2 + Nothing -> 3 diff --git a/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.snap b/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.snap new file mode 100644 index 00000000..2966fee2 --- /dev/null +++ b/tests-integration/fixtures/checking2/031_exhaustive_case_redundant/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: forall (t1 :: Type). Maybe (t1 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: RedundantPatterns { + patterns: [ + "Just _", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + InferringExpression( + AstId(13), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.purs b/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.purs new file mode 100644 index 00000000..eea38f36 --- /dev/null +++ b/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.purs @@ -0,0 +1,6 @@ +module Main where + +data Maybe a = Just a | Nothing + +test :: Maybe Int -> Int +test (Just _) = 1 diff --git a/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.snap b/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.snap new file mode 100644 index 00000000..9791e2b7 --- /dev/null +++ b/tests-integration/fixtures/checking2/032_exhaustive_equation_signature/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Maybe Int -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.purs b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.purs new file mode 100644 index 00000000..a1286577 --- /dev/null +++ b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Data.Boolean (otherwise) + +data Maybe a = Just a | Nothing + +test1 = case _ of + Just _ | otherwise -> 1 + +test2 = case _ of + Nothing | true -> 2 diff --git a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap new file mode 100644 index 00000000..39bd5d9d --- /dev/null +++ b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap @@ -0,0 +1,48 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test1 :: forall (t1 :: Type). Maybe (t1 :: Type) -> Int +test2 :: forall (t2 :: Type). Maybe (t2 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(8), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + InferringExpression( + AstId(19), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(10), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + InferringExpression( + AstId(36), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.purs b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.purs new file mode 100644 index 00000000..05cc6527 --- /dev/null +++ b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Maybe a = Just a | Nothing + +test = + let + Just x = Just 1 + in + x diff --git a/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap new file mode 100644 index 00000000..db613c2e --- /dev/null +++ b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + InferringExpression( + AstId(13), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.purs b/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.purs new file mode 100644 index 00000000..562b493e --- /dev/null +++ b/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.purs @@ -0,0 +1,8 @@ +module Main where + +data List a = Cons a (List a) | Nil + +infixr 5 Cons as : + +head :: forall a. List a -> a +head (x : _) = x diff --git a/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.snap b/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.snap new file mode 100644 index 00000000..e1ad071c --- /dev/null +++ b/tests-integration/fixtures/checking2/035_exhaustive_operator_constructor/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Cons :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +Nil :: forall (a :: Type). List (a :: Type) +: :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +head :: forall (a :: Type). List (a :: Type) -> (a :: Type) + +Types +List :: Type -> Type + +Roles +List = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(9), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 92ace862..7d954d12 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -87,3 +87,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_028_value_check_main() { run_test("028_value_check", "Main"); } #[rustfmt::skip] #[test] fn test_029_operator_check_main() { run_test("029_operator_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_030_exhaustive_case_infer_main() { run_test("030_exhaustive_case_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_031_exhaustive_case_redundant_main() { run_test("031_exhaustive_case_redundant", "Main"); } + +#[rustfmt::skip] #[test] fn test_032_exhaustive_equation_signature_main() { run_test("032_exhaustive_equation_signature", "Main"); } + +#[rustfmt::skip] #[test] fn test_033_exhaustive_guards_otherwise_main() { run_test("033_exhaustive_guards_otherwise", "Main"); } + +#[rustfmt::skip] #[test] fn test_034_exhaustive_let_pattern_main() { run_test("034_exhaustive_let_pattern", "Main"); } + +#[rustfmt::skip] #[test] fn test_035_exhaustive_operator_constructor_main() { run_test("035_exhaustive_operator_constructor", "Main"); } From f405ae9d4c0543516c9ec0c4176c1200202cdcbf Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 03:05:30 +0800 Subject: [PATCH 273/386] Split intern_function_chain and intern_function_chain_iter --- compiler-core/checking2/src/context.rs | 8 +++++++- compiler-core/checking2/src/source/term_items.rs | 5 +---- compiler-core/checking2/src/source/terms.rs | 4 ++-- compiler-core/checking2/src/source/terms/equations.rs | 5 ++--- compiler-core/checking2/src/source/terms/form_ado.rs | 3 +-- compiler-core/checking2/src/source/terms/form_do.rs | 6 +++--- compiler-core/checking2/src/source/terms/form_let.rs | 2 +- compiler-core/checking2/src/source/terms/forms.rs | 4 ++-- compiler-core/checking2/src/source/type_items.rs | 6 +++--- 9 files changed, 22 insertions(+), 21 deletions(-) diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index a2b7cc9b..3e8f9694 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -168,7 +168,13 @@ where } /// Interns a right-associated function chain from arguments to result. - pub fn intern_function_chain(&self, arguments: I, result: TypeId) -> TypeId + pub fn intern_function_chain(&self, arguments: &[TypeId], result: TypeId) -> TypeId { + let arguments = arguments.iter().copied(); + self.intern_function_chain_iter(arguments, result) + } + + /// Interns a right-associated function chain from iterator arguments to result. + pub fn intern_function_chain_iter(&self, arguments: I, result: TypeId) -> TypeId where I: IntoIterator, I::IntoIter: DoubleEndedIterator, diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index cba98b03..8df977c0 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -161,10 +161,7 @@ where let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function(state, context, quantified)?; - let function = { - let arguments = arguments.iter().copied(); - context.intern_function_chain(arguments, result) - }; + let function = context.intern_function_chain(&arguments, result); equations::check_equations_core( state, diff --git a/compiler-core/checking2/src/source/terms.rs b/compiler-core/checking2/src/source/terms.rs index 635fbd72..fa26db90 100644 --- a/compiler-core/checking2/src/source/terms.rs +++ b/compiler-core/checking2/src/source/terms.rs @@ -81,7 +81,7 @@ where unification::subtype(state, context, result_type, current)?; - let function_type = context.intern_function_chain(parameters.iter().copied(), result_type); + let function_type = context.intern_function_chain(¶meters, result_type); Ok(function_type) } @@ -181,7 +181,7 @@ where let result_type = infer_expression_core(state, context, expression)?; let result_type = toolkit::instantiate_constrained(state, context, result_type)?; - Ok(context.intern_function_chain(parameter_types.iter().copied(), result_type)) + Ok(context.intern_function_chain(¶meter_types, result_type)) } fn infer_expression_core( diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index 0c03ff0b..b407e950 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -37,8 +37,7 @@ where let result_type = state.fresh_unification(context.queries, context.prim.t); let argument_types = &argument_types[..minimum_equation_arity]; - let equation_type = - context.intern_function_chain(argument_types.iter().copied(), result_type); + let equation_type = context.intern_function_chain(argument_types, result_type); unification::subtype(state, context, equation_type, group_type)?; if let Some(guarded) = &equation.guarded { @@ -103,7 +102,7 @@ where result } else { let remaining = &arguments[equation_arity..]; - context.intern_function_chain(remaining.iter().copied(), result) + context.intern_function_chain(remaining, result) }; if let Some(guarded) = &equation.guarded { diff --git a/compiler-core/checking2/src/source/terms/form_ado.rs b/compiler-core/checking2/src/source/terms/form_ado.rs index 65c5be4e..01289ab4 100644 --- a/compiler-core/checking2/src/source/terms/form_ado.rs +++ b/compiler-core/checking2/src/source/terms/form_ado.rs @@ -107,8 +107,7 @@ where // in_expression_type := ?in_expression // lambda_type := ?a -> ?b -> ?in_expression let in_expression_type = state.fresh_unification(context.queries, context.prim.t); - let lambda_type = - context.intern_function_chain(binder_types.iter().copied(), in_expression_type); + let lambda_type = context.intern_function_chain(&binder_types, in_expression_type); // The desugared form of an ado-expression is a forward applicative // pipeline, unlike do-notation which works inside-out. The example diff --git a/compiler-core/checking2/src/source/terms/form_do.rs b/compiler-core/checking2/src/source/terms/form_do.rs index 5b14659b..352e6b52 100644 --- a/compiler-core/checking2/src/source/terms/form_do.rs +++ b/compiler-core/checking2/src/source/terms/form_do.rs @@ -45,7 +45,7 @@ where let m_a = context.intern_application(m, a); let m_b = context.intern_application(m, b); let a_to_m_b = context.intern_function(a, m_b); - Ok(context.intern_function_chain([m_a, a_to_m_b], m_b)) + Ok(context.intern_function_chain(&[m_a, a_to_m_b], m_b)) } } @@ -80,7 +80,7 @@ where let f_a = context.intern_application(f, a); let f_b = context.intern_application(f, b); let a_to_b = context.intern_function(a, b); - Ok(context.intern_function_chain([a_to_b, f_a], f_b)) + Ok(context.intern_function_chain(&[a_to_b, f_a], f_b)) } } @@ -103,7 +103,7 @@ where let f_a_to_b = context.intern_application(f, a_to_b); let f_a = context.intern_application(f, a); let f_b = context.intern_application(f, b); - Ok(context.intern_function_chain([f_a_to_b, f_a], f_b)) + Ok(context.intern_function_chain(&[f_a_to_b, f_a], f_b)) } } diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index d7ab0d26..74edec30 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -145,7 +145,7 @@ where let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function(state, context, quantified)?; - let function = context.intern_function_chain(arguments.iter().copied(), result); + let function = context.intern_function_chain(&arguments, result); equations::check_equations_core( state, diff --git a/compiler-core/checking2/src/source/terms/forms.rs b/compiler-core/checking2/src/source/terms/forms.rs index 8711c90a..4a98d900 100644 --- a/compiler-core/checking2/src/source/terms/forms.rs +++ b/compiler-core/checking2/src/source/terms/forms.rs @@ -84,7 +84,7 @@ where state.fresh_unification(context.queries, context.prim.t) }; - let function_type = context.intern_function_chain(argument_types.iter().copied(), result_type); + let function_type = context.intern_function_chain(&argument_types, result_type); let exhaustiveness = exhaustive::check_lambda_patterns(state, context, &argument_types, binders)?; @@ -131,7 +131,7 @@ where state.fresh_unification(context.queries, context.prim.t) }; - let function_type = context.intern_function_chain(arguments.iter().copied(), result_type); + let function_type = context.intern_function_chain(&arguments, result_type); let exhaustiveness = exhaustive::check_lambda_patterns(state, context, &arguments, binders)?; diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 7d50e310..8f4ddce1 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -381,7 +381,7 @@ where { let bindings = check_type_variable_bindings(state, context, bindings, &[])?; let kinds = bindings.iter().map(|binder| binder.kind); - let inferred = context.intern_function_chain(kinds, context.prim.t); + let inferred = context.intern_function_chain_iter(kinds, context.prim.t); if let Some(expected) = state.checked.lookup_type(item_id) { unification::subtype(state, context, inferred, expected)?; @@ -614,7 +614,7 @@ where let bindings = check_type_variable_bindings(state, context, bindings, &[])?; let kinds = bindings.iter().map(|binder| binder.kind); let result = state.fresh_unification(context.queries, context.prim.t); - let inferred = context.intern_function_chain(kinds, result); + let inferred = context.intern_function_chain_iter(kinds, result); if let Some(expected) = state.checked.lookup_type(item_id) { unification::subtype(state, context, inferred, expected)?; @@ -704,7 +704,7 @@ where { let bindings = check_type_variable_bindings(state, context, bindings, &[])?; let kinds = bindings.iter().map(|binder| binder.kind); - let inferred = context.intern_function_chain(kinds, context.prim.constraint); + let inferred = context.intern_function_chain_iter(kinds, context.prim.constraint); if let Some(expected) = state.checked.lookup_type(item_id) { unification::subtype(state, context, inferred, expected)?; From de15f1fe5474b889e6923b59435104785146e410 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 03:36:07 +0800 Subject: [PATCH 274/386] Implement defer for partial synonym applications --- compiler-core/checking2/src/source/synonym.rs | 58 +++++++++++++++++-- .../036_synonym_partial_defer/Main.purs | 13 +++++ .../036_synonym_partial_defer/Main.snap | 50 ++++++++++++++++ .../tests/checking2/generated.rs | 2 + 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.purs create mode 100644 tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index b8402e40..83f15f20 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -1,5 +1,6 @@ //! Implements syntax-driven checking rules for synonym detection. +use std::mem; use std::sync::Arc; use building_types::QueryResult; @@ -52,16 +53,21 @@ pub fn infer_synonym_constructor( where Q: ExternalQueries, { - if is_recursive_synonym(context, file_id, item_id)? { - state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); - } - if arity > 0 { + if state.defer_synonym_expansion { + let synonym_type = context.queries.intern_type(Type::Constructor(file_id, item_id)); + return Ok((synonym_type, kind)); + } + state.insert_error(ErrorKind::PartialSynonymApplication { id }); let unknown = context.unknown("partial synonym application"); return Ok((unknown, unknown)); } + if is_recursive_synonym(context, file_id, item_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); + } + let synonym = Synonym { saturation: Saturation::Full, reference: (file_id, item_id), @@ -89,6 +95,16 @@ where } if arguments.len() < arity { + if state.defer_synonym_expansion { + return infer_partial_synonym_application( + state, + context, + (file_id, type_id), + function_kind, + arguments, + ); + } + state.insert_error(ErrorKind::PartialSynonymApplication { id }); let unknown = context.unknown("partial synonym application"); return Ok((unknown, unknown)); @@ -97,12 +113,16 @@ where let (synonym_arguments, excess_arguments) = arguments.split_at(arity); let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let (argument_types, (_, synonym_kind)) = infer_synonym_application_chain( + let defer_synonym_expansion = mem::replace(&mut state.defer_synonym_expansion, true); + let chain_result = infer_synonym_application_chain( state, context, (function_type, function_kind), synonym_arguments, - )?; + ); + state.defer_synonym_expansion = defer_synonym_expansion; + + let (argument_types, (_, synonym_kind)) = chain_result?; let synonym = Synonym { saturation: Saturation::Full, @@ -122,6 +142,32 @@ where Ok((synonym_type, synonym_kind)) } +fn infer_partial_synonym_application( + state: &mut CheckState, + context: &CheckContext, + (file_id, type_id): (FileId, TypeItemId), + function_kind: TypeId, + arguments: &[lowering::TypeId], +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let (argument_types, (_, synonym_kind)) = + infer_synonym_application_chain(state, context, (function_type, function_kind), arguments)?; + + let synonym = Synonym { + saturation: Saturation::Partial, + reference: (file_id, type_id), + arguments: Arc::from(argument_types), + }; + + let synonym_id = context.intern_synonym(synonym); + let synonym_type = context.intern_synonym_application(synonym_id); + + Ok((synonym_type, synonym_kind)) +} + fn infer_synonym_application_chain( state: &mut CheckState, context: &CheckContext, diff --git a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.purs b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.purs new file mode 100644 index 00000000..21f54fc5 --- /dev/null +++ b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.purs @@ -0,0 +1,13 @@ +module Main where + +data Identity a = Identity a + +type ReaderT :: Type -> (Type -> Type) -> Type -> Type +type ReaderT r m a = r -> m a + +type Apply :: forall k1 k2. (k1 -> k2) -> k1 -> k2 +type Apply f a = f a + +type Good = Apply (Apply (ReaderT Int) Identity) String + +type Bad = ReaderT Int diff --git a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap new file mode 100644 index 00000000..39ec3c0e --- /dev/null +++ b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap @@ -0,0 +1,50 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) + +Types +Identity :: Type -> Type +ReaderT :: Type -> (Type -> Type) -> Type -> Type +Apply :: + forall (k1 :: Type) (k2 :: Type). ((k1 :: Type) -> (k2 :: Type)) -> (k1 :: Type) -> (k2 :: Type) +Good :: Type +Bad :: ?[partial synonym application] + +Synonyms +type ReaderT r m a = (r :: Type) -> (m :: Type -> Type) (a :: Type) +type Apply f a = (f :: (t4 :: Type) -> (t5 :: Type)) (a :: (t4 :: Type)) +type Good = Apply (Apply (ReaderT Int) Identity) String +type Bad = ?[partial synonym application] + +Roles +Identity = [Representational] + +Errors +CheckError { + kind: PartialSynonymApplication { + id: AstId(60), + }, + crumbs: [ + CheckingKind( + AstId(60), + ), + InferringKind( + AstId(60), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + CheckingKind( + AstId(60), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 7d954d12..b634e4d4 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -99,3 +99,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_034_exhaustive_let_pattern_main() { run_test("034_exhaustive_let_pattern", "Main"); } #[rustfmt::skip] #[test] fn test_035_exhaustive_operator_constructor_main() { run_test("035_exhaustive_operator_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_036_synonym_partial_defer_main() { run_test("036_synonym_partial_defer", "Main"); } From 9440938688ba2a29a163fcbe61366ac521d36463 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 17:11:44 +0800 Subject: [PATCH 275/386] Organise CheckState fields --- .../checking2/src/core/exhaustive.rs | 2 +- compiler-core/checking2/src/implication.rs | 1 + compiler-core/checking2/src/source/synonym.rs | 10 +++++----- .../checking2/src/source/type_items.rs | 2 +- compiler-core/checking2/src/source/types.rs | 8 ++++---- compiler-core/checking2/src/state.rs | 20 ++++++++++--------- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/compiler-core/checking2/src/core/exhaustive.rs b/compiler-core/checking2/src/core/exhaustive.rs index e43308f3..cc016c5b 100644 --- a/compiler-core/checking2/src/core/exhaustive.rs +++ b/compiler-core/checking2/src/core/exhaustive.rs @@ -167,7 +167,7 @@ impl MissingConstructor { } pub type PatternId = interner::Id; -pub type PatternStorage = interner::Interner; +pub type PatternInterner = interner::Interner; type PatternVector = Vec; type PatternMatrix = Vec; diff --git a/compiler-core/checking2/src/implication.rs b/compiler-core/checking2/src/implication.rs index c2482215..4ebe2ac8 100644 --- a/compiler-core/checking2/src/implication.rs +++ b/compiler-core/checking2/src/implication.rs @@ -23,6 +23,7 @@ impl Implication { } } +/// Keeps track of implications for the type checker. pub struct Implications { nodes: Vec, current: ImplicationId, diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 83f15f20..c8abfe60 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -54,7 +54,7 @@ where Q: ExternalQueries, { if arity > 0 { - if state.defer_synonym_expansion { + if state.defer_expansion { let synonym_type = context.queries.intern_type(Type::Constructor(file_id, item_id)); return Ok((synonym_type, kind)); } @@ -95,7 +95,7 @@ where } if arguments.len() < arity { - if state.defer_synonym_expansion { + if state.defer_expansion { return infer_partial_synonym_application( state, context, @@ -111,16 +111,16 @@ where } let (synonym_arguments, excess_arguments) = arguments.split_at(arity); - let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let defer_synonym_expansion = mem::replace(&mut state.defer_synonym_expansion, true); + + let defer_expansion = mem::replace(&mut state.defer_expansion, true); let chain_result = infer_synonym_application_chain( state, context, (function_type, function_kind), synonym_arguments, ); - state.defer_synonym_expansion = defer_synonym_expansion; + state.defer_expansion = defer_expansion; let (argument_types, (_, synonym_kind)) = chain_result?; diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 8f4ddce1..171cf2dc 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -322,7 +322,7 @@ where let kind = resolve_type_variable_binding(state, context, signature_kind, equation_binding)?; let name = state.names.fresh(); - state.kind_scope.bind_forall(equation_binding.id, name, kind); + state.bindings.bind_forall(equation_binding.id, name, kind); let text = if let Some(name) = &equation_binding.name { SmolStr::clone(name) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 748bbd79..7f072cfd 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -238,7 +238,7 @@ where lowering::TypeKind::Variable { name, resolution } => match resolution { Some(lowering::TypeVariableResolution::Forall(forall)) => { let (n, k) = state - .kind_scope + .bindings .lookup_forall(*forall) .expect("invariant violated: KindScope::bind_forall"); @@ -252,13 +252,13 @@ where let n = state.names.fresh(); let k = state.fresh_unification(context.queries, context.prim.t); - state.kind_scope.bind_implicit(implicit.node, implicit.id, n, k); + state.bindings.bind_implicit(implicit.node, implicit.id, n, k); let t = context.intern_rigid(n, state.depth, k); Ok((t, k)) } else { let (n, k) = state - .kind_scope + .bindings .lookup_implicit(implicit.node, implicit.id) .expect("invariant violated: KindScope::bind_implicit"); @@ -366,7 +366,7 @@ where let text = if let Some(name) = &binding.name { SmolStr::clone(name) } else { name.as_text() }; let text = context.queries.intern_smol_str(text); - state.kind_scope.bind_forall(binding.id, name, kind); + state.bindings.bind_forall(binding.id, name, kind); Ok(ForallBinder { visible, name, text, kind }) } diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index bab89f13..15024fa2 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -9,7 +9,7 @@ use rustc_hash::FxHashMap; use crate::context::CheckContext; use crate::core::exhaustive::{ - ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternKind, PatternStorage, + ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternInterner, PatternKind, }; use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty, zonk}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; @@ -83,13 +83,13 @@ impl Unifications { /// Tracks type variable bindings during kind inference. #[derive(Default)] -pub struct KindScope { +pub struct Bindings { forall_bindings: FxHashMap, implicit_bindings: FxHashMap<(lowering::GraphNodeId, lowering::ImplicitBindingId), (Name, TypeId)>, } -impl KindScope { +impl Bindings { pub fn bind_forall(&mut self, id: lowering::TypeVariableBindingId, name: Name, kind: TypeId) { self.forall_bindings.insert(id, (name, kind)); } @@ -122,11 +122,13 @@ pub struct CheckState { pub checked: CheckedModule, pub names: Names, + pub bindings: Bindings, + pub patterns: PatternInterner, + pub unifications: Unifications, pub implications: Implications, - pub patterns: PatternStorage, - pub kind_scope: KindScope, - pub defer_synonym_expansion: bool, + + pub defer_expansion: bool, pub depth: Depth, pub crumbs: Vec, @@ -137,11 +139,11 @@ impl CheckState { CheckState { checked: Default::default(), names: Names::new(file_id), + bindings: Default::default(), + patterns: Default::default(), unifications: Default::default(), implications: Default::default(), - patterns: Default::default(), - kind_scope: Default::default(), - defer_synonym_expansion: Default::default(), + defer_expansion: Default::default(), depth: Depth(0), crumbs: Default::default(), } From 50f823a432c08ff28bcd91bf08bdb4a05eb2fc39 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 19:43:02 +0800 Subject: [PATCH 276/386] Fix generalisation bug for Scc::Mutual --- .../checking2/src/core/generalise.rs | 75 ++++++++++++++++--- .../checking2/src/source/term_items.rs | 27 ++++++- .../checking2/src/source/type_items.rs | 10 ++- .../037_value_recursive_check/Main.purs | 4 + .../037_value_recursive_check/Main.snap | 9 +++ .../038_value_recursive_infer/Main.purs | 3 + .../038_value_recursive_infer/Main.snap | 9 +++ .../039_value_mutual_check/Main.purs | 7 ++ .../039_value_mutual_check/Main.snap | 10 +++ .../040_value_mutual_infer/Main.purs | 5 ++ .../040_value_mutual_infer/Main.snap | 10 +++ .../checking2/041_data_mutual_check/Main.purs | 7 ++ .../checking2/041_data_mutual_check/Main.snap | 20 +++++ .../checking2/042_data_mutual_infer/Main.purs | 5 ++ .../checking2/042_data_mutual_infer/Main.snap | 20 +++++ .../tests/checking2/generated.rs | 12 +++ 16 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 tests-integration/fixtures/checking2/037_value_recursive_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/037_value_recursive_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/038_value_recursive_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/038_value_recursive_infer/Main.snap create mode 100644 tests-integration/fixtures/checking2/039_value_mutual_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/039_value_mutual_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/040_value_mutual_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/040_value_mutual_infer/Main.snap create mode 100644 tests-integration/fixtures/checking2/041_data_mutual_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/041_data_mutual_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/042_data_mutual_infer/Main.purs create mode 100644 tests-integration/fixtures/checking2/042_data_mutual_infer/Main.snap diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 50a50ec4..0b43ef40 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -26,7 +26,7 @@ use rustc_hash::FxHashSet; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{ForallBinder, Type, TypeId, normalise, zonk}; -use crate::state::{CheckState, UnificationEntry}; +use crate::state::{CheckState, UnificationEntry, UnificationState}; type UniGraph = DiGraphMap; @@ -124,12 +124,16 @@ where aux(graph, state, context, id, None, &mut visited_kinds) } -/// Generalises a given type. See also module-level documentation. -pub fn generalise( +/// Collect the unsolved unification variables in a type. +/// +/// This function returns the unification variables topologically sorted +/// based on their dependencies, such as when unification variables appear +/// in another unification variable's kind. +pub fn unsolved_unifications( state: &mut CheckState, context: &CheckContext, id: TypeId, -) -> QueryResult +) -> QueryResult> where Q: ExternalQueries, { @@ -137,13 +141,36 @@ where collect_unification_into(&mut graph, state, context, id)?; if graph.node_count() == 0 { - return Ok(id); + return Ok(vec![]); } let Ok(unsolved) = algo::toposort(&graph, None) else { - return Ok(id); + return Ok(vec![]); }; + Ok(unsolved) +} + +/// Generalise a type with the given unification variables. +/// +/// The `unsolved` parameter should be sourced from [`unsolved_unifications`]. +/// This split is necessary for generalisation on mutually-recursive bindings. +/// Note that while this function expects unsolved unification variables, it +/// also handles solved ones gracefully in the event that they become solved +/// before being generalised. +pub fn generalise_unsolved( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + unsolved: &[u32], +) -> QueryResult +where + Q: ExternalQueries, +{ + if unsolved.is_empty() { + return Ok(id); + } + let mut quantified = id; // All rigid type variables in a single generalisation share the same @@ -169,18 +196,44 @@ where let depth = state.depth.increment(); for &unification_id in unsolved.iter() { - let UnificationEntry { kind, .. } = *state.unifications.get(unification_id); + let UnificationEntry { kind, state: unification_state, .. } = + *state.unifications.get(unification_id); + + let (name, kind) = match unification_state { + UnificationState::Unsolved => { + let name = state.names.fresh(); + let rigid = context.intern_rigid(name, depth, kind); + state.unifications.solve(unification_id, rigid); + (name, kind) + } + UnificationState::Solved(solution) => { + let solution = normalise::normalise(state, context, solution)?; + let Type::Rigid(name, _, kind) = context.lookup_type(solution) else { + continue; + }; + (name, kind) + } + }; - let name = state.names.fresh(); let text = context.queries.intern_smol_str(name.as_text()); let binder = ForallBinder { visible: false, name, text, kind }; let binder = context.intern_forall_binder(binder); quantified = context.intern_forall(binder, quantified); - - let rigid = context.intern_rigid(name, depth, kind); - state.unifications.solve(unification_id, rigid); } zonk::zonk(state, context, quantified) } + +/// Generalises a given type. See also module-level documentation. +pub fn generalise( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let unsolved = unsolved_unifications(state, context, id)?; + generalise_unsolved(state, context, id, &unsolved) +} diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 8df977c0..1726ea15 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -29,6 +29,10 @@ where check_term_signature(state, context, item)?; } + if scc.is_recursive() { + prepare_binding_group(state, context, &items); + } + let mut term_scc = TermSccState::default(); for &item in items { @@ -42,6 +46,19 @@ where Ok(()) } +fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TermItemId]) +where + Q: ExternalQueries, +{ + for &item_id in items { + if state.checked.terms.contains_key(&item_id) { + continue; + } + let t = state.fresh_unification(context.queries, context.prim.t); + state.checked.terms.insert(item_id, t); + } +} + fn check_term_signature( state: &mut CheckState, context: &CheckContext, @@ -200,13 +217,21 @@ fn finalise_term_binding_group( where Q: ExternalQueries, { + let mut pending = Vec::with_capacity(items.len()); + for &item_id in items { let Some(kind) = state.checked.terms.get(&item_id).copied() else { continue; }; let kind = zonk::zonk(state, context, kind)?; - let kind = generalise::generalise(state, context, kind)?; + let unsolved = generalise::unsolved_unifications(state, context, kind)?; + + pending.push((item_id, kind, unsolved)); + } + + for (item_id, kind, unsolved) in pending { + let kind = generalise::generalise_unsolved(state, context, kind, &unsolved)?; state.checked.terms.insert(item_id, kind); } diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 171cf2dc..f315b761 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -112,13 +112,21 @@ fn finalise_type_binding_group( where Q: ExternalQueries, { + let mut pending = Vec::with_capacity(items.len()); + for &item_id in items { let Some(kind) = state.checked.types.get(&item_id).copied() else { continue; }; let kind = zonk::zonk(state, context, kind)?; - let kind = generalise::generalise(state, context, kind)?; + let unsolved = generalise::unsolved_unifications(state, context, kind)?; + + pending.push((item_id, kind, unsolved)); + } + + for (item_id, kind, unsolved) in pending { + let kind = generalise::generalise_unsolved(state, context, kind, &unsolved)?; state.checked.types.insert(item_id, kind); } diff --git a/tests-integration/fixtures/checking2/037_value_recursive_check/Main.purs b/tests-integration/fixtures/checking2/037_value_recursive_check/Main.purs new file mode 100644 index 00000000..4dc32cc7 --- /dev/null +++ b/tests-integration/fixtures/checking2/037_value_recursive_check/Main.purs @@ -0,0 +1,4 @@ +module Main where + +id :: forall a. a -> a +id a = id a diff --git a/tests-integration/fixtures/checking2/037_value_recursive_check/Main.snap b/tests-integration/fixtures/checking2/037_value_recursive_check/Main.snap new file mode 100644 index 00000000..67d59fe6 --- /dev/null +++ b/tests-integration/fixtures/checking2/037_value_recursive_check/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +id :: forall (a :: Type). (a :: Type) -> (a :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.purs b/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.purs new file mode 100644 index 00000000..1c95e3b5 --- /dev/null +++ b/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.purs @@ -0,0 +1,3 @@ +module Main where + +id a = id a diff --git a/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.snap b/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.snap new file mode 100644 index 00000000..469b9c4e --- /dev/null +++ b/tests-integration/fixtures/checking2/038_value_recursive_infer/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +id :: forall (t1 :: Type) (t0 :: Type). (t1 :: Type) -> (t0 :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/039_value_mutual_check/Main.purs b/tests-integration/fixtures/checking2/039_value_mutual_check/Main.purs new file mode 100644 index 00000000..5a9680fa --- /dev/null +++ b/tests-integration/fixtures/checking2/039_value_mutual_check/Main.purs @@ -0,0 +1,7 @@ +module Main where + +f :: forall a. a -> a +f a = g a + +g :: forall a. a -> a +g a = f a diff --git a/tests-integration/fixtures/checking2/039_value_mutual_check/Main.snap b/tests-integration/fixtures/checking2/039_value_mutual_check/Main.snap new file mode 100644 index 00000000..0b4fe448 --- /dev/null +++ b/tests-integration/fixtures/checking2/039_value_mutual_check/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +f :: forall (a :: Type). (a :: Type) -> (a :: Type) +g :: forall (a :: Type). (a :: Type) -> (a :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.purs b/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.purs new file mode 100644 index 00000000..aa56cab5 --- /dev/null +++ b/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.purs @@ -0,0 +1,5 @@ +module Main where + +f a = g a + +g a = f a diff --git a/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.snap b/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.snap new file mode 100644 index 00000000..1145a8a6 --- /dev/null +++ b/tests-integration/fixtures/checking2/040_value_mutual_infer/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +f :: forall (t1 :: Type) (t0 :: Type). (t1 :: Type) -> (t0 :: Type) +g :: forall (t1 :: Type) (t0 :: Type). (t1 :: Type) -> (t0 :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/041_data_mutual_check/Main.purs b/tests-integration/fixtures/checking2/041_data_mutual_check/Main.purs new file mode 100644 index 00000000..296fb613 --- /dev/null +++ b/tests-integration/fixtures/checking2/041_data_mutual_check/Main.purs @@ -0,0 +1,7 @@ +module Main where + +data F :: forall k. k -> Type +data F a = F (G a) + +data G :: forall k. k -> Type +data G a = G (F a) diff --git a/tests-integration/fixtures/checking2/041_data_mutual_check/Main.snap b/tests-integration/fixtures/checking2/041_data_mutual_check/Main.snap new file mode 100644 index 00000000..c5ac4c1c --- /dev/null +++ b/tests-integration/fixtures/checking2/041_data_mutual_check/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +F :: + forall (k :: Type) (a :: (k :: Type)). + G @(k :: Type) (a :: (k :: Type)) -> F @(k :: Type) (a :: (k :: Type)) +G :: + forall (k :: Type) (a :: (k :: Type)). + F @(k :: Type) (a :: (k :: Type)) -> G @(k :: Type) (a :: (k :: Type)) + +Types +F :: forall (k :: Type). (k :: Type) -> Type +G :: forall (k :: Type). (k :: Type) -> Type + +Roles +F = [Representational] +G = [Representational] diff --git a/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.purs b/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.purs new file mode 100644 index 00000000..105e9ec2 --- /dev/null +++ b/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.purs @@ -0,0 +1,5 @@ +module Main where + +data F a = F (G a) + +data G a = G (F a) diff --git a/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.snap b/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.snap new file mode 100644 index 00000000..6f8626c6 --- /dev/null +++ b/tests-integration/fixtures/checking2/042_data_mutual_infer/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +F :: + forall (t2 :: Type) (a :: (t2 :: Type)). + G (a :: (t2 :: Type)) -> F @(t2 :: Type) (a :: (t2 :: Type)) +G :: + forall (t2 :: Type) (a :: (t2 :: Type)). + F (a :: (t2 :: Type)) -> G @(t2 :: Type) (a :: (t2 :: Type)) + +Types +F :: forall (t2 :: Type). (t2 :: Type) -> Type +G :: forall (t2 :: Type). (t2 :: Type) -> Type + +Roles +F = [Representational] +G = [Representational] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index b634e4d4..5bcac04c 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -101,3 +101,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_035_exhaustive_operator_constructor_main() { run_test("035_exhaustive_operator_constructor", "Main"); } #[rustfmt::skip] #[test] fn test_036_synonym_partial_defer_main() { run_test("036_synonym_partial_defer", "Main"); } + +#[rustfmt::skip] #[test] fn test_037_value_recursive_check_main() { run_test("037_value_recursive_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_038_value_recursive_infer_main() { run_test("038_value_recursive_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_039_value_mutual_check_main() { run_test("039_value_mutual_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_040_value_mutual_infer_main() { run_test("040_value_mutual_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_041_data_mutual_check_main() { run_test("041_data_mutual_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_042_data_mutual_infer_main() { run_test("042_data_mutual_infer", "Main"); } From f065a14e0942e25784a98885f2e0d369c9377d2a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 22:28:42 +0800 Subject: [PATCH 277/386] Introduce quantification for class canonical types --- .../checking2/src/source/type_items.rs | 16 ++++-- .../checking2/009_class_check/Main.snap | 2 +- .../checking2/010_class_infer/Main.snap | 2 +- .../checking2/011_class_superclass/Main.snap | 4 +- .../012_class_polykind_check/Main.snap | 2 +- .../013_class_polykind_infer/Main.snap | 2 +- tests-integration/src/generated/basic.rs | 50 +++++++++++++++---- 7 files changed, 59 insertions(+), 19 deletions(-) diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index f315b761..e1314f16 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -797,17 +797,25 @@ where }) .collect_vec(); - let mut canonical = context.queries.intern_type(Type::Constructor(context.id, item_id)); + let mut class_head = context.queries.intern_type(Type::Constructor(context.id, item_id)); for binder in &class_binders { let rigid = context.intern_rigid(binder.name, state.depth, binder.kind); - canonical = context.intern_kind_application(canonical, rigid); + class_head = context.intern_kind_application(class_head, rigid); } for (index, parameter) in parameters.iter().enumerate() { let kind = get_parameter_kind(index); let rigid = context.intern_rigid(parameter.name, state.depth, kind); - canonical = context.intern_application(canonical, rigid); + class_head = context.intern_application(class_head, rigid); + } + + let mut canonical = class_head; + for type_parameter in type_parameters.iter().rev() { + canonical = context.intern_forall(*type_parameter, canonical); + } + for kind_binder in kind_binders.iter().rev() { + canonical = context.intern_forall(*kind_binder, canonical); } let superclasses = superclasses @@ -821,7 +829,7 @@ where let toolkit::InspectQuantified { binders: member_binders, quantified: member_inner } = toolkit::inspect_quantified(state, context, member_type)?; - let mut result = context.intern_constrained(canonical, member_inner); + let mut result = context.intern_constrained(class_head, member_inner); for member_binder in member_binders.iter().copied().rev() { let binder_id = context.intern_forall_binder(member_binder); diff --git a/tests-integration/fixtures/checking2/009_class_check/Main.snap b/tests-integration/fixtures/checking2/009_class_check/Main.snap index db102bf8..6b36e9fe 100644 --- a/tests-integration/fixtures/checking2/009_class_check/Main.snap +++ b/tests-integration/fixtures/checking2/009_class_check/Main.snap @@ -10,5 +10,5 @@ Types Eq :: Type -> Constraint Classes -class Eq (t0 :: Type) +class forall (a :: Type). Eq (a :: Type) eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/010_class_infer/Main.snap b/tests-integration/fixtures/checking2/010_class_infer/Main.snap index db102bf8..6b36e9fe 100644 --- a/tests-integration/fixtures/checking2/010_class_infer/Main.snap +++ b/tests-integration/fixtures/checking2/010_class_infer/Main.snap @@ -10,5 +10,5 @@ Types Eq :: Type -> Constraint Classes -class Eq (t0 :: Type) +class forall (a :: Type). Eq (a :: Type) eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/011_class_superclass/Main.snap b/tests-integration/fixtures/checking2/011_class_superclass/Main.snap index bac92513..31498ca6 100644 --- a/tests-integration/fixtures/checking2/011_class_superclass/Main.snap +++ b/tests-integration/fixtures/checking2/011_class_superclass/Main.snap @@ -12,7 +12,7 @@ Eq :: Type -> Constraint Ord :: Type -> Constraint Classes -class Eq (t0 :: Type) +class forall (a :: Type). Eq (a :: Type) eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean -class Ord (t1 :: Type) <= Eq (t1 :: Type) +class forall (a :: Type). Eq (a :: Type) <= Ord (a :: Type) compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int diff --git a/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap index d1faf52d..16d6251f 100644 --- a/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap +++ b/tests-integration/fixtures/checking2/012_class_polykind_check/Main.snap @@ -13,7 +13,7 @@ Types HasKind :: forall (k :: Type). (k :: Type) -> Constraint Classes -class HasKind @(t0 :: Type) (t1 :: (t0 :: Type)) +class forall (k :: Type) (a :: (k :: Type)). HasKind @(k :: Type) (a :: (k :: Type)) reflectKind :: forall (k :: Type) (a :: (k :: Type)) (p :: (k :: Type) -> Type). HasKind @(k :: Type) (a :: (k :: Type)) => diff --git a/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap index 5ee12b39..304280cc 100644 --- a/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap +++ b/tests-integration/fixtures/checking2/013_class_polykind_infer/Main.snap @@ -13,7 +13,7 @@ Types HasKind' :: forall (t2 :: Type). (t2 :: Type) -> Constraint Classes -class HasKind' @(t2 :: Type) (t0 :: (t2 :: Type)) +class forall (t2 :: Type) (a :: (t2 :: Type)). HasKind' @(t2 :: Type) (a :: (t2 :: Type)) reflectKind' :: forall (t2 :: Type) (a :: (t2 :: Type)) (p :: (t2 :: Type) -> Type). HasKind' @(t2 :: Type) (a :: (t2 :: Type)) => diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index af19978d..f557b0cf 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -3,6 +3,7 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; use checking2::ExternalQueries; +use checking2::core as core2; use checking2::core::pretty as pretty2; use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; @@ -258,19 +259,50 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { .. }) in indexed.items.iter_types() { let Some(class) = checked.lookup_class(id) else { continue }; - let canonical = pretty2::Pretty::new(engine).render(class.canonical); + let class_binders = + class.kind_binders.iter().chain(class.type_parameters.iter()).copied().collect_vec(); - let mut superclasses = String::new(); - if !class.superclasses.is_empty() { - let formatted = class - .superclasses + let binder_names = class_binders + .iter() + .map(|&binder_id| { + let binder = engine.lookup_forall_binder(binder_id); + (binder.name, engine.lookup_smol_str(binder.text)) + }) + .collect_vec(); + + let render_with_binders = |type_id| { + pretty2::Pretty::new(engine).names(binder_names.iter().cloned()).render(type_id) + }; + + let mut class_head = class.canonical; + while let core2::Type::Forall(_, inner) = engine.lookup_type(class_head) { + class_head = inner; + } + + let canonical = render_with_binders(class_head); + let forall_prefix = if class_binders.is_empty() { + String::new() + } else { + let binders = class_binders .iter() - .map(|&superclass| pretty2::Pretty::new(engine).render(superclass)) - .collect_vec(); - superclasses = format!(" <= {}", formatted.join(", ")); + .map(|&binder_id| { + let binder = engine.lookup_forall_binder(binder_id); + let text = engine.lookup_smol_str(binder.text); + let kind = render_with_binders(binder.kind); + format!("({text} :: {kind})") + }) + .join(" "); + format!("forall {binders}. ") + }; + + if class.superclasses.is_empty() { + writeln!(out, "class {forall_prefix}{canonical}").unwrap(); + } else { + let superclasses = + class.superclasses.iter().map(|&superclass| render_with_binders(superclass)).join(", "); + writeln!(out, "class {forall_prefix}{superclasses} <= {canonical}").unwrap(); } - writeln!(out, "class {canonical}{superclasses}").unwrap(); for &mid in &class.members { let Some(member_name) = indexed.items[mid].name.as_deref() else { continue }; let Some(member_type) = checked.lookup_term(mid) else { continue }; From 8454b29edaaf82a2c908b5125c052905a6f620e2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 26 Feb 2026 20:42:14 +0800 Subject: [PATCH 278/386] Implement initial instance declaration checking --- compiler-core/checking2/src/core.rs | 9 ++ .../checking2/src/core/generalise.rs | 106 +++++++++++++++- compiler-core/checking2/src/lib.rs | 11 +- .../checking2/src/source/term_items.rs | 117 +++++++++++++++++- 4 files changed, 237 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 81585629..78a811ac 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -137,6 +137,15 @@ pub struct CheckedClass { pub members: Vec, } +/// Represents a checked instance declaration head. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckedInstance { + /// Type class reference. + pub resolution: (FileId, TypeItemId), + /// Canonical instance type, e.g. `forall a. Eq a => Eq (Array a)`. + pub canonical: TypeId, +} + /// The core type representation used by the checker after name resolution. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Type { diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 0b43ef40..0aed8b86 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -25,7 +25,8 @@ use rustc_hash::FxHashSet; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, Type, TypeId, normalise, zonk}; +use crate::core::walk::{self, TypeWalker, WalkAction}; +use crate::core::{ForallBinder, Name, Type, TypeId, normalise, zonk}; use crate::state::{CheckState, UnificationEntry, UnificationState}; type UniGraph = DiGraphMap; @@ -237,3 +238,106 @@ where let unsolved = unsolved_unifications(state, context, id)?; generalise_unsolved(state, context, id, &unsolved) } + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum ImplicitOrUnification { + Implicit(Name, TypeId), + Unification(u32, TypeId), +} + +#[derive(Default)] +struct GeneraliseImplicit { + owner: Option, + graph: DiGraphMap, + bound: FxHashSet, +} + +impl TypeWalker for GeneraliseImplicit { + fn visit( + &mut self, + state: &mut CheckState, + context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + match t { + Type::Rigid(name, _, kind) => { + let next_owner = ImplicitOrUnification::Implicit(*name, *kind); + let prev_owner = self.owner.replace(next_owner); + + self.graph.add_node(next_owner); + if let Some(prev_owner) = prev_owner { + self.graph.add_edge(prev_owner, next_owner, ()); + } + + walk::walk_type(state, context, *kind, self)?; + self.owner = prev_owner; + } + Type::Unification(id) => { + let UnificationEntry { kind, .. } = state.unifications.get(*id); + + let next_owner = ImplicitOrUnification::Unification(*id, *kind); + let prev_owner = self.owner.replace(next_owner); + + self.graph.add_node(next_owner); + if let Some(prev_owner) = prev_owner { + self.graph.add_edge(prev_owner, next_owner, ()); + } + + walk::walk_type(state, context, *kind, self)?; + self.owner = prev_owner; + } + _ => {} + } + Ok(WalkAction::Continue) + } + + fn visit_binder(&mut self, binder: &ForallBinder) { + self.bound.insert(binder.name); + } +} + +pub fn generalise_implicit( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut walker = GeneraliseImplicit::default(); + walk::walk_type(state, context, id, &mut walker)?; + + let Ok(implicits_unifications) = algo::toposort(&walker.graph, None) else { + return Ok(context.unknown("invalid recursive graph")); + }; + + let depth = state.depth.increment(); + + let mut binders = vec![]; + for implicit_unification in implicits_unifications { + match implicit_unification { + ImplicitOrUnification::Implicit(name, kind) => { + let text = context.queries.intern_smol_str(name.as_text()); + binders.push(ForallBinder { visible: false, name, text, kind }) + } + ImplicitOrUnification::Unification(id, kind) => { + let name = state.names.fresh(); + let text = context.queries.intern_smol_str(name.as_text()); + let rigid = context.intern_rigid(name, depth, kind); + state.unifications.solve(id, rigid); + binders.push(ForallBinder { visible: false, name, text, kind }) + } + } + } + + let id = binders.into_iter().fold(id, |inner, binder| { + let binder = context.intern_forall_binder(binder); + context.intern_forall(binder, inner) + }); + + zonk::zonk(state, context, id) +} diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 01f6eb24..3d8fb46d 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -13,14 +13,14 @@ use std::sync::Arc; use building_types::{QueryProxy, QueryResult}; use files::FileId; -use indexing::{TermItemId, TypeItemId}; +use indexing::{InstanceId, TermItemId, TypeItemId}; use resolving::ResolvedModule; use rustc_hash::FxHashMap; use smol_str::SmolStr; use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, RowTypeId, Synonym, - SynonymId, Type, TypeId, + CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, + RowTypeId, Synonym, SynonymId, Type, TypeId, }; use crate::error::CheckError; @@ -60,6 +60,7 @@ pub struct CheckedModule { pub terms: FxHashMap, pub synonyms: FxHashMap, pub classes: FxHashMap, + pub instances: FxHashMap, pub roles: FxHashMap>, pub nodes: CheckedNodes, pub errors: Vec, @@ -101,6 +102,10 @@ impl CheckedModule { self.classes.get(&id).cloned() } + pub fn lookup_instance(&self, id: InstanceId) -> Option { + self.instances.get(&id).cloned() + } + pub fn lookup_roles(&self, id: TypeItemId) -> Option> { self.roles.get(&id).cloned() } diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 1726ea15..94f2f344 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -2,12 +2,12 @@ use std::mem; use building_types::QueryResult; use files::FileId; -use indexing::TermItemId; +use indexing::{TermItemId, TermItemKind, TypeItemId}; use lowering::TermItemIr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, generalise, toolkit, unification, zonk}; +use crate::core::{CheckedInstance, Type, TypeId, generalise, toolkit, unification, zonk}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::terms::equations; use crate::source::types; @@ -19,6 +19,119 @@ struct TermSccState { } pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + check_instance_declarations(state, context)?; + check_value_groups(state, context)?; + Ok(()) +} + +pub fn check_instance_declarations( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.term_scc { + let items = scc.as_slice(); + + let items = items.iter().filter_map(|&item_id| { + let Some(item) = context.lowered.info.get_term_item(item_id) else { + return None; + }; + let TermItemIr::Instance { constraints, resolution, arguments, .. } = item else { + return None; + }; + let resolution = *resolution; + Some(CheckInstanceDeclaration { item_id, constraints, resolution, arguments }) + }); + + for item in items { + check_instance_declaration(state, context, item)?; + } + } + + Ok(()) +} + +struct CheckInstanceDeclaration<'a> { + item_id: TermItemId, + constraints: &'a [lowering::TypeId], + resolution: Option<(FileId, TypeItemId)>, + arguments: &'a [lowering::TypeId], +} + +fn check_instance_declaration( + state: &mut CheckState, + context: &CheckContext, + item: CheckInstanceDeclaration, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let CheckInstanceDeclaration { item_id, constraints, resolution, arguments } = item; + + let Some((class_file, class_id)) = resolution else { + return Ok(()); + }; + + let TermItemKind::Instance { id: instance_id } = context.indexed.items[item_id].kind else { + return Ok(()); + }; + + let class_kind = toolkit::lookup_file_type(state, context, class_file, class_id)?; + + let expected_kinds = { + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, class_kind)?; + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, quantified)?; + arguments + }; + + if expected_kinds.len() != arguments.len() { + state.insert_error(ErrorKind::InstanceHeadMismatch { + class_file, + class_item: class_id, + expected: expected_kinds.len(), + actual: arguments.len(), + }); + } + + let mut class_type = context.queries.intern_type(Type::Constructor(class_file, class_id)); + let mut class_kind = class_kind; + + for &argument in arguments { + (class_type, class_kind) = + types::infer_application_kind(state, context, (class_type, class_kind), argument)?; + } + + unification::subtype(state, context, class_kind, context.prim.constraint)?; + + let mut checked_constraints = Vec::with_capacity(constraints.len()); + for &constraint in constraints { + let (constraint_type, _) = + types::check_kind(state, context, constraint, context.prim.constraint)?; + checked_constraints.push(constraint_type); + } + + let mut canonical = class_type; + for &constraint in checked_constraints.iter().rev() { + canonical = context.intern_constrained(constraint, canonical); + } + + let resolution = (class_file, class_id); + let canonical = zonk::zonk(state, context, canonical)?; + let canonical = generalise::generalise_implicit(state, context, canonical)?; + + state.checked.instances.insert(instance_id, CheckedInstance { resolution, canonical }); + + Ok(()) +} + +fn check_value_groups(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> where Q: ExternalQueries, { From f705853b4015ed79810b99fc0f8dff986ede508e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 27 Feb 2026 04:09:37 +0800 Subject: [PATCH 279/386] Add integration test for instance declaration checking --- .../checking2/043_instance_check/Main.purs | 11 +++++++++++ .../checking2/043_instance_check/Main.snap | 19 +++++++++++++++++++ tests-integration/src/generated/basic.rs | 10 ++++++++++ .../tests/checking2/generated.rs | 2 ++ 4 files changed, 42 insertions(+) create mode 100644 tests-integration/fixtures/checking2/043_instance_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/043_instance_check/Main.snap diff --git a/tests-integration/fixtures/checking2/043_instance_check/Main.purs b/tests-integration/fixtures/checking2/043_instance_check/Main.purs new file mode 100644 index 00000000..8a03919b --- /dev/null +++ b/tests-integration/fixtures/checking2/043_instance_check/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a + +instance eqArray :: Eq a => Eq (Array a) + +class TypeEq :: forall k. k -> k -> Constraint +class TypeEq a b | a -> b, b -> a + +instance TypeEq a a diff --git a/tests-integration/fixtures/checking2/043_instance_check/Main.snap b/tests-integration/fixtures/checking2/043_instance_check/Main.snap new file mode 100644 index 00000000..4e93d4fd --- /dev/null +++ b/tests-integration/fixtures/checking2/043_instance_check/Main.snap @@ -0,0 +1,19 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Eq :: Type -> Constraint +TypeEq :: forall (k :: Type). (k :: Type) -> (k :: Type) -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) +class forall (k :: Type) (a :: (k :: Type)) (b :: (k :: Type)). TypeEq @(k :: Type) (a :: (k :: Type)) (b :: (k :: Type)) + +Instances +instance forall (t4 :: Type). Eq (t4 :: Type) => Eq (Array (t4 :: Type)) +instance forall (t6 :: Type) (t5 :: (t6 :: Type)). + TypeEq @(t6 :: Type) (t5 :: (t6 :: Type)) (t5 :: (t6 :: Type)) diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index f557b0cf..4f25e6bf 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -311,6 +311,16 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { } } + if !checked.instances.is_empty() { + writeln!(out, "\nInstances").unwrap(); + } + let mut instance_entries: Vec<_> = checked.instances.iter().collect(); + instance_entries.sort_by_key(|(id, _)| *id); + for (_instance_id, instance) in instance_entries { + let canonical = pretty2::Pretty::new(engine).render(instance.canonical); + writeln!(out, "instance {canonical}").unwrap(); + } + if !checked.roles.is_empty() { writeln!(out, "\nRoles").unwrap(); } diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 5bcac04c..dc066bee 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -113,3 +113,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_041_data_mutual_check_main() { run_test("041_data_mutual_check", "Main"); } #[rustfmt::skip] #[test] fn test_042_data_mutual_infer_main() { run_test("042_data_mutual_infer", "Main"); } + +#[rustfmt::skip] #[test] fn test_043_instance_check_main() { run_test("043_instance_check", "Main"); } From ee76e85e4dc5da892ec90ea4a8930f930b4eea9f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 00:41:03 +0800 Subject: [PATCH 280/386] Track canonical names in CheckedModule --- compiler-core/checking2/src/core.rs | 2 - .../checking2/src/core/generalise.rs | 10 ++-- compiler-core/checking2/src/core/pretty.rs | 45 ++++++++--------- compiler-core/checking2/src/lib.rs | 9 +++- .../checking2/src/source/type_items.rs | 3 +- compiler-core/checking2/src/source/types.rs | 3 +- compiler-core/checking2/src/state.rs | 2 +- .../015_operator_alias_invalid_kind/Main.snap | 2 +- .../Main.snap | 6 +-- .../033_exhaustive_guards_otherwise/Main.snap | 2 +- .../036_synonym_partial_defer/Main.snap | 2 +- tests-integration/src/generated/basic.rs | 50 +++++++++---------- 12 files changed, 66 insertions(+), 70 deletions(-) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 78a811ac..d4227b1b 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -52,8 +52,6 @@ pub struct ForallBinder { pub visible: bool, /// The unique identity attached to the type variable. pub name: Name, - /// The source-level text of the type variable. - pub text: SmolStrId, /// The kind of the type variable. pub kind: TypeId, } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 0aed8b86..ecc7e4d3 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -216,9 +216,7 @@ where } }; - let text = context.queries.intern_smol_str(name.as_text()); - - let binder = ForallBinder { visible: false, name, text, kind }; + let binder = ForallBinder { visible: false, name, kind }; let binder = context.intern_forall_binder(binder); quantified = context.intern_forall(binder, quantified); } @@ -321,15 +319,13 @@ where for implicit_unification in implicits_unifications { match implicit_unification { ImplicitOrUnification::Implicit(name, kind) => { - let text = context.queries.intern_smol_str(name.as_text()); - binders.push(ForallBinder { visible: false, name, text, kind }) + binders.push(ForallBinder { visible: false, name, kind }) } ImplicitOrUnification::Unification(id, kind) => { let name = state.names.fresh(); - let text = context.queries.intern_smol_str(name.as_text()); let rigid = context.intern_rigid(name, depth, kind); state.unifications.solve(id, rigid); - binders.push(ForallBinder { visible: false, name, text, kind }) + binders.push(ForallBinder { visible: false, name, kind }) } } } diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 7f93ae11..514c4ce7 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -6,11 +6,11 @@ use pretty::{Arena, DocAllocator, DocBuilder}; use rustc_hash::FxHashMap; use smol_str::{SmolStr, SmolStrBuilder}; -use crate::ExternalQueries; use crate::core::{ ForallBinder, ForallBinderId, Name, RowField, RowType, RowTypeId, SmolStrId, Synonym, SynonymId, Type, TypeId, }; +use crate::{CheckedModule, ExternalQueries}; type Doc<'a> = DocBuilder<'a, Arena<'a>, ()>; @@ -18,12 +18,12 @@ pub struct Pretty<'a, Q> { queries: &'a Q, width: usize, signature: Option<&'a str>, - names: FxHashMap, + checked: &'a CheckedModule, } impl<'a, Q: ExternalQueries> Pretty<'a, Q> { - pub fn new(queries: &'a Q) -> Self { - Pretty { queries, width: 100, signature: None, names: FxHashMap::default() } + pub fn new(queries: &'a Q, checked: &'a CheckedModule) -> Self { + Pretty { queries, width: 100, signature: None, checked } } pub fn width(mut self, width: usize) -> Self { @@ -36,15 +36,9 @@ impl<'a, Q: ExternalQueries> Pretty<'a, Q> { self } - pub fn names(mut self, names: impl IntoIterator) -> Self { - self.names.extend(names); - self - } - pub fn render(self, id: TypeId) -> SmolStr { let arena = Arena::new(); - let mut printer = Printer::new(&arena, self.queries); - printer.names = self.names; + let mut printer = Printer::new(&arena, self.queries, &self.checked.names); let document = if let Some(name) = self.signature { printer.signature(name, id) @@ -75,15 +69,19 @@ where { arena: &'a Arena<'a>, queries: &'a Q, - names: FxHashMap, + names: &'a FxHashMap, } impl<'a, Q> Printer<'a, Q> where Q: ExternalQueries, { - fn new(arena: &'a Arena<'a>, queries: &'a Q) -> Printer<'a, Q> { - Printer { arena, queries, names: FxHashMap::default() } + fn new( + arena: &'a Arena<'a>, + queries: &'a Q, + names: &'a FxHashMap, + ) -> Printer<'a, Q> { + Printer { arena, queries, names } } fn lookup_type(&self, id: TypeId) -> Type { @@ -239,10 +237,12 @@ where } Type::Rigid(name, _, kind) => { - let name = self.names.entry(name).or_insert_with(|| name.as_text()); - let name = SmolStr::clone(name); + let text = match self.names.get(&name) { + Some(&id) => self.lookup_smol_str(id), + None => name.as_text(), + }; let kind = self.traverse(Precedence::Top, kind); - self.arena.text(format!("({name} :: ")).append(kind).append(self.arena.text(")")) + self.arena.text(format!("({text} :: ")).append(kind).append(self.arena.text(")")) } Type::Unification(unification_id) => self.arena.text(format!("?{unification_id}")), @@ -358,16 +358,13 @@ where inner = next_inner; } - // Register source-level names so rigid variables in the body - // display their original names instead of synthetic ones. - for binder in &binders { - self.names.insert(binder.name, self.lookup_smol_str(binder.text)); - } - let binders = binders .iter() .map(|binder| { - let text = self.lookup_smol_str(binder.text); + let text = match self.names.get(&binder.name) { + Some(&id) => self.lookup_smol_str(id), + None => binder.name.as_text(), + }; let kind = self.traverse(Precedence::Top, binder.kind); self.arena .text(format!("({} :: ", text)) diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 3d8fb46d..1c14ccd6 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -19,8 +19,8 @@ use rustc_hash::FxHashMap; use smol_str::SmolStr; use crate::core::{ - CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, ForallBinderId, Role, RowType, - RowTypeId, Synonym, SynonymId, Type, TypeId, + CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, ForallBinderId, Name, Role, + RowType, RowTypeId, SmolStrId, Synonym, SynonymId, Type, TypeId, }; use crate::error::CheckError; @@ -64,6 +64,7 @@ pub struct CheckedModule { pub roles: FxHashMap>, pub nodes: CheckedNodes, pub errors: Vec, + pub names: FxHashMap, } #[derive(Debug, Default, PartialEq, Eq)] @@ -109,6 +110,10 @@ impl CheckedModule { pub fn lookup_roles(&self, id: TypeItemId) -> Option> { self.roles.get(&id).cloned() } + + pub fn lookup_name(&self, name: Name) -> Option { + self.names.get(&name).copied() + } } impl CheckedNodes { diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index e1314f16..368a048b 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -339,9 +339,10 @@ where }; let text = context.queries.intern_smol_str(text); + state.checked.names.insert(name, text); let visible = equation_binding.visible; - binders.push(ForallBinder { visible, name, text, kind }); + binders.push(ForallBinder { visible, name, kind }); } Ok(binders) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 7f072cfd..1e35605a 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -366,8 +366,9 @@ where let text = if let Some(name) = &binding.name { SmolStr::clone(name) } else { name.as_text() }; let text = context.queries.intern_smol_str(text); + state.checked.names.insert(name, text); state.bindings.bind_forall(binding.id, name, kind); - Ok(ForallBinder { visible, name, text, kind }) + Ok(ForallBinder { visible, name, kind }) } pub fn infer_application_kind( diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 15024fa2..38fef314 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -204,7 +204,7 @@ impl CheckState { Q: ExternalQueries, { let id = zonk::zonk(self, context, id)?; - let pretty = pretty::Pretty::new(context.queries).render(id); + let pretty = pretty::Pretty::new(context.queries, &self.checked).render(id); Ok(context.queries.intern_smol_str(pretty)) } diff --git a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap index cc51886b..74e2af52 100644 --- a/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap +++ b/tests-integration/fixtures/checking2/015_operator_alias_invalid_kind/Main.snap @@ -15,7 +15,7 @@ type Identity a = (a :: (t1 :: Type)) Errors CheckError { kind: InvalidTypeOperator { - kind_message: Id(5), + kind_message: Id(4), }, crumbs: [], } diff --git a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap index e85beffc..50181c3e 100644 --- a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap +++ b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap @@ -12,6 +12,6 @@ Chain :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) -> (k :: ChainParen :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) -> (k :: Type) Synonyms -type Add a b = (a :: (t0 :: Type)) -type Chain a b c = (a :: (t3 :: Type)) + (b :: (t3 :: Type)) + (c :: (t3 :: Type)) -type ChainParen a b c = (a :: (t7 :: Type)) + (b :: (t7 :: Type)) + (c :: (t7 :: Type)) +type Add a b = (a :: (k :: Type)) +type Chain a b c = (a :: (k :: Type)) + (b :: (k :: Type)) + (c :: (k :: Type)) +type ChainParen a b c = (a :: (k :: Type)) + (b :: (k :: Type)) + (c :: (k :: Type)) diff --git a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap index 39bd5d9d..3c588e95 100644 --- a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap +++ b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap @@ -34,7 +34,7 @@ CheckError { CheckError { kind: MissingPatterns { patterns: [ - Id(10), + Id(9), ], }, crumbs: [ diff --git a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap index 39ec3c0e..c63d1d4d 100644 --- a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap +++ b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap @@ -16,7 +16,7 @@ Bad :: ?[partial synonym application] Synonyms type ReaderT r m a = (r :: Type) -> (m :: Type -> Type) (a :: Type) -type Apply f a = (f :: (t4 :: Type) -> (t5 :: Type)) (a :: (t4 :: Type)) +type Apply f a = (f :: (k1 :: Type) -> (k2 :: Type)) (a :: (k1 :: Type)) type Good = Apply (Apply (ReaderT Int) Identity) String type Bad = ?[partial synonym application] diff --git a/tests-integration/src/generated/basic.rs b/tests-integration/src/generated/basic.rs index 4f25e6bf..6f6a2d98 100644 --- a/tests-integration/src/generated/basic.rs +++ b/tests-integration/src/generated/basic.rs @@ -2,9 +2,8 @@ use std::fmt::Write; use analyzer::{QueryEngine, locate}; use checking::core::pretty; -use checking2::ExternalQueries; -use checking2::core as core2; use checking2::core::pretty as pretty2; +use checking2::{ExternalQueries, core as core2}; use diagnostics::{DiagnosticsContext, ToDiagnostics, format_rustc}; use files::FileId; use indexing::{ImportKind, TermItem, TypeItem, TypeItemId, TypeItemKind}; @@ -220,13 +219,26 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { let indexed = engine.indexed(id).unwrap(); let checked = engine.checked2(id).unwrap(); + let name_text = |name: core2::Name| -> String { + checked + .lookup_name(name) + .map(|id| engine.lookup_smol_str(id).to_string()) + .unwrap_or_else(|| name.as_text().to_string()) + }; + + let pretty = |type_id| pretty2::Pretty::new(engine, &checked).render(type_id); + + let pretty_signature = |name: &str, type_id| { + pretty2::Pretty::new(engine, &checked).signature(name).render(type_id) + }; + let mut out = String::default(); writeln!(out, "Terms").unwrap(); for (id, TermItem { name, .. }) in indexed.items.iter_terms() { let Some(name) = name else { continue }; let Some(kind) = checked.lookup_term(id) else { continue }; - let signature = pretty2::Pretty::new(engine).signature(name).render(kind); + let signature = pretty_signature(name, kind); writeln!(out, "{signature}").unwrap(); } @@ -234,7 +246,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(kind) = checked.lookup_type(id) else { continue }; - let signature = pretty2::Pretty::new(engine).signature(name).render(kind); + let signature = pretty_signature(name, kind); writeln!(out, "{signature}").unwrap(); } @@ -244,10 +256,8 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { for (id, TypeItem { name, .. }) in indexed.items.iter_types() { let Some(name) = name else { continue }; let Some(definition) = checked.lookup_synonym(id) else { continue }; - let names = definition.parameters.iter().map(|b| (b.name, engine.lookup_smol_str(b.text))); - let replacement = pretty2::Pretty::new(engine).names(names).render(definition.synonym); - let binders = - definition.parameters.iter().map(|b| engine.lookup_smol_str(b.text)).collect_vec(); + let replacement = pretty(definition.synonym); + let binders = definition.parameters.iter().map(|b| name_text(b.name)).collect_vec(); let binders_formatted = if binders.is_empty() { String::new() } else { format!(" {}", binders.join(" ")) }; writeln!(out, "type {name}{binders_formatted} = {replacement}").unwrap(); @@ -262,24 +272,12 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { let class_binders = class.kind_binders.iter().chain(class.type_parameters.iter()).copied().collect_vec(); - let binder_names = class_binders - .iter() - .map(|&binder_id| { - let binder = engine.lookup_forall_binder(binder_id); - (binder.name, engine.lookup_smol_str(binder.text)) - }) - .collect_vec(); - - let render_with_binders = |type_id| { - pretty2::Pretty::new(engine).names(binder_names.iter().cloned()).render(type_id) - }; - let mut class_head = class.canonical; while let core2::Type::Forall(_, inner) = engine.lookup_type(class_head) { class_head = inner; } - let canonical = render_with_binders(class_head); + let canonical = pretty(class_head); let forall_prefix = if class_binders.is_empty() { String::new() } else { @@ -287,8 +285,8 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { .iter() .map(|&binder_id| { let binder = engine.lookup_forall_binder(binder_id); - let text = engine.lookup_smol_str(binder.text); - let kind = render_with_binders(binder.kind); + let text = name_text(binder.name); + let kind = pretty(binder.kind); format!("({text} :: {kind})") }) .join(" "); @@ -299,14 +297,14 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { writeln!(out, "class {forall_prefix}{canonical}").unwrap(); } else { let superclasses = - class.superclasses.iter().map(|&superclass| render_with_binders(superclass)).join(", "); + class.superclasses.iter().map(|&superclass| pretty(superclass)).join(", "); writeln!(out, "class {forall_prefix}{superclasses} <= {canonical}").unwrap(); } for &mid in &class.members { let Some(member_name) = indexed.items[mid].name.as_deref() else { continue }; let Some(member_type) = checked.lookup_term(mid) else { continue }; - let signature = pretty2::Pretty::new(engine).signature(member_name).render(member_type); + let signature = pretty_signature(member_name, member_type); writeln!(out, " {signature}").unwrap(); } } @@ -317,7 +315,7 @@ pub fn report_checked2(engine: &QueryEngine, id: FileId) -> String { let mut instance_entries: Vec<_> = checked.instances.iter().collect(); instance_entries.sort_by_key(|(id, _)| *id); for (_instance_id, instance) in instance_entries { - let canonical = pretty2::Pretty::new(engine).render(instance.canonical); + let canonical = pretty(instance.canonical); writeln!(out, "instance {canonical}").unwrap(); } From 51da0b047a47809db58ce37f4f2aeba10287e8e1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 15:36:55 +0800 Subject: [PATCH 281/386] Implement functional dependencies and closure computation --- compiler-core/checking2/src/core.rs | 1 + .../checking2/src/core/constraint.rs | 1 + .../core/constraint/functional_dependency.rs | 148 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 compiler-core/checking2/src/core/constraint.rs create mode 100644 compiler-core/checking2/src/core/constraint/functional_dependency.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index d4227b1b..826e6b6f 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -1,5 +1,6 @@ //! Implements core type structures. +pub mod constraint; pub mod exhaustive; pub mod fold; pub mod generalise; diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs new file mode 100644 index 00000000..b25501df --- /dev/null +++ b/compiler-core/checking2/src/core/constraint.rs @@ -0,0 +1 @@ +pub mod functional_dependency; diff --git a/compiler-core/checking2/src/core/constraint/functional_dependency.rs b/compiler-core/checking2/src/core/constraint/functional_dependency.rs new file mode 100644 index 00000000..22ec2341 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/functional_dependency.rs @@ -0,0 +1,148 @@ +use std::collections::HashSet; + +use crate::safe_loop; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Fd { + pub determiners: HashSet, + pub determined: HashSet, +} + +impl Fd { + pub fn new( + determiners: impl IntoIterator, + determined: impl IntoIterator, + ) -> Fd { + Fd { + determiners: determiners.into_iter().collect(), + determined: determined.into_iter().collect(), + } + } +} + +pub fn get_all_determined(functional_dependencies: &[Fd]) -> HashSet { + functional_dependencies.iter().flat_map(|fd| fd.determined.iter().copied()).collect() +} + +pub fn compute_closure( + functional_dependencies: &[Fd], + initial_positions: &HashSet, +) -> HashSet { + let mut determined = initial_positions.clone(); + safe_loop! { + let mut changed = false; + + for functional_dependency in functional_dependencies { + if functional_dependency.determiners.is_subset(&determined) { + for &position in &functional_dependency.determined { + if determined.insert(position) { + changed = true; + } + } + } + } + + if !changed { + return determined; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_no_fundeps() { + let initial: HashSet = [0, 1].into_iter().collect(); + let result = compute_closure(&[], &initial); + assert_eq!(result, initial); + } + + #[test] + fn test_single_fundep() { + let fundeps = vec![Fd::new([0], [1])]; + let initial: HashSet = [0].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0, 1].into_iter().collect()); + } + + #[test] + fn test_fundep_not_triggered() { + let fundeps = vec![Fd::new([0], [1])]; + let initial: HashSet = [1].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [1].into_iter().collect()); + } + + #[test] + fn test_transitive_fundeps() { + let fundeps = vec![Fd::new([0], [1]), Fd::new([1], [2])]; + let initial: HashSet = [0].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0, 1, 2].into_iter().collect()); + } + + #[test] + fn test_multi_determiner() { + let fundeps = vec![Fd::new([0, 1], [2])]; + + let initial: HashSet = [0].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0].into_iter().collect()); + + let initial: HashSet = [0, 1].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0, 1, 2].into_iter().collect()); + } + + #[test] + fn test_multiple_determined() { + let fundeps = vec![Fd::new([0], [1, 2])]; + let initial: HashSet = [0].into_iter().collect(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0, 1, 2].into_iter().collect()); + } + + #[test] + fn test_empty_determiners() { + let fundeps = vec![Fd::new([], [0])]; + let initial: HashSet = HashSet::new(); + let result = compute_closure(&fundeps, &initial); + assert_eq!(result, [0].into_iter().collect()); + } + + #[test] + fn test_all_determined_no_fundeps() { + let result = get_all_determined(&[]); + assert_eq!(result, HashSet::new()); + } + + #[test] + fn test_all_determined_single_fundep() { + let fundeps = vec![Fd::new([0], [1])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1].into_iter().collect()); + } + + #[test] + fn test_all_determined_multiple_fundeps() { + let fundeps = vec![Fd::new([0], [1]), Fd::new([1], [2])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1, 2].into_iter().collect()); + } + + #[test] + fn test_all_determined_overlapping() { + let fundeps = vec![Fd::new([0], [1, 2]), Fd::new([3], [1])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [1, 2].into_iter().collect()); + } + + #[test] + fn test_all_determined_empty_determiners() { + let fundeps = vec![Fd::new([], [0])]; + let result = get_all_determined(&fundeps); + assert_eq!(result, [0].into_iter().collect()); + } +} From 5f691a16ea38decfc31f21b111a2549fad7fcfb1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 19:53:06 +0800 Subject: [PATCH 282/386] Use NameToType reference in SubstituteName --- compiler-core/checking2/src/core/substitute.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking2/src/core/substitute.rs b/compiler-core/checking2/src/core/substitute.rs index 2d4c494b..ce3ccc05 100644 --- a/compiler-core/checking2/src/core/substitute.rs +++ b/compiler-core/checking2/src/core/substitute.rs @@ -17,11 +17,11 @@ pub type NameToType = FxHashMap; /// Names are globally unique, removing the need for scope tracking and /// removing the need for capture-avoiding substitutions. This property /// is extremely useful for for instantiation. -pub struct SubstituteName { - bindings: NameToType, +pub struct SubstituteName<'a> { + bindings: &'a NameToType, } -impl SubstituteName { +impl SubstituteName<'_> { pub fn one( state: &mut CheckState, context: &CheckContext, @@ -33,13 +33,13 @@ impl SubstituteName { Q: ExternalQueries, { let bindings = NameToType::from_iter([(name, replacement)]); - fold_type(state, context, in_type, &mut SubstituteName { bindings }) + fold_type(state, context, in_type, &mut SubstituteName { bindings: &bindings }) } pub fn many( state: &mut CheckState, context: &CheckContext, - bindings: NameToType, + bindings: &NameToType, in_type: TypeId, ) -> QueryResult where @@ -49,7 +49,7 @@ impl SubstituteName { } } -impl TypeFold for SubstituteName { +impl TypeFold for SubstituteName<'_> { fn transform( &mut self, _state: &mut CheckState, From e4c5724993918247c81517059d8d026fe24b2ecc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 14:29:15 +0800 Subject: [PATCH 283/386] Port initial constraint solver from checking --- .../checking2/src/core/constraint.rs | 1145 +++++++++++++++++ .../checking2/src/core/exhaustive/convert.rs | 10 +- .../checking2/src/core/generalise.rs | 148 ++- compiler-core/checking2/src/core/toolkit.rs | 49 +- .../checking2/src/source/term_items.rs | 105 +- compiler-core/checking2/src/state.rs | 9 +- .../030_exhaustive_case_infer/Main.snap | 2 +- .../033_exhaustive_guards_otherwise/Main.snap | 4 +- .../034_exhaustive_let_pattern/Main.snap | 2 +- 9 files changed, 1436 insertions(+), 38 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index b25501df..47f5836e 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -1 +1,1146 @@ pub mod functional_dependency; + +use std::collections::{HashSet, VecDeque}; +use std::mem; + +use building_types::QueryResult; +use files::FileId; +use indexing::{InstanceChainId, TypeItemId}; +use itertools::Itertools; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::context::CheckContext; +use crate::core::substitute::{NameToType, SubstituteName}; +use crate::core::walk::{self, TypeWalker, WalkAction}; +use crate::core::{ + CheckedInstance, ForallBinder, Name, Type, TypeId, normalise, toolkit, unification, +}; +use crate::error::ErrorKind; +use crate::implication::ImplicationId; +use crate::state::CheckState; +use crate::{CheckedModule, ExternalQueries, safe_loop}; + +use functional_dependency::{Fd, compute_closure, get_all_determined}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ConstraintApplication { + pub file_id: FileId, + pub item_id: TypeItemId, + pub arguments: Vec, +} + +#[derive(Debug, Clone)] +enum MatchInstance { + Match { constraints: Vec, equalities: Vec<(TypeId, TypeId)> }, + Apart, + Stuck, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MatchType { + Match, + Apart, + Stuck, +} + +impl MatchType { + fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { + if let MatchType::Match = self { f() } else { Ok(self) } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CanUnify { + Equal, + Apart, + Unify, +} + +impl CanUnify { + fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { + if let CanUnify::Equal = self { f() } else { Ok(self) } + } +} + +#[derive(Clone)] +struct CandidateInstance { + chain_id: Option, + position: u32, + checked: CheckedInstance, +} + +struct DecomposedInstance { + binders: Vec, + arguments: Vec, + constraints: Vec, +} + +pub fn solve_implication( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let implication = state.implications.current(); + solve_implication_id(state, context, implication, &[]) +} + +/// Recursively solves an implication and its children. +fn solve_implication_id( + state: &mut CheckState, + context: &CheckContext, + implication: ImplicationId, + inherited: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let (wanted, given, children) = { + let node = &mut state.implications[implication]; + (mem::take(&mut node.wanted), mem::take(&mut node.given), node.children.clone()) + }; + + let all_given = inherited.iter().copied().chain(given.iter().copied()).collect_vec(); + + // Solve this implication's children with all_given. + for child in &children { + let residual = solve_implication_id(state, context, *child, &all_given)?; + + // TODO: partition_by_skolem_escape once skolems are introduced. + state.implications[implication].wanted.extend(residual); + } + + // Solve this implication's wanted constraints with all_given. + let remaining = mem::take(&mut state.implications[implication].wanted); + let wanted: VecDeque<_> = wanted.into_iter().chain(remaining).collect(); + let residuals = solve_constraints(state, context, wanted, &all_given)?; + + let implication = &mut state.implications[implication]; + implication.given = given; + implication.wanted = residuals.iter().copied().collect(); + + Ok(residuals) +} + +fn solve_constraints( + state: &mut CheckState, + context: &CheckContext, + wanted: VecDeque, + given: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let given = elaborate_given(state, context, given)?; + let mut work_queue = wanted; + let mut residual = vec![]; + + loop { + let mut made_progress = false; + + 'work: while let Some(wanted) = work_queue.pop_front() { + let Some(application) = constraint_application(state, context, wanted)? else { + residual.push(wanted); + continue; + }; + + match match_given_instances(state, context, &application, &given)? { + Some(MatchInstance::Match { equalities, .. }) => { + for (t1, t2) in equalities { + if unification::unify(state, context, t1, t2)? { + made_progress = true; + } + } + continue 'work; + } + Some(MatchInstance::Apart | MatchInstance::Stuck) | None => {} + } + + let instance_chains = collect_instance_chains(state, context, &application)?; + + for chain in instance_chains { + 'chain: for instance in chain { + match match_instance(state, context, &application, &instance)? { + MatchInstance::Match { constraints, equalities } => { + for (t1, t2) in equalities { + if unification::unify(state, context, t1, t2)? { + made_progress = true; + } + } + work_queue.extend(constraints); + continue 'work; + } + MatchInstance::Apart => continue 'chain, + MatchInstance::Stuck => break 'chain, + } + } + } + + residual.push(wanted); + } + + if made_progress && !residual.is_empty() { + work_queue.extend(residual.drain(..)); + } else { + break; + } + } + + Ok(residual) +} + +pub fn constraint_application( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let (constructor, arguments) = toolkit::extract_type_application(state, context, id)?; + let constructor = normalise::normalise(state, context, constructor)?; + Ok(match context.lookup_type(constructor) { + Type::Constructor(file_id, item_id) => { + Some(ConstraintApplication { file_id, item_id, arguments }) + } + _ => None, + }) +} + +/// Discovers implied constraints from given constraints. +fn elaborate_given( + state: &mut CheckState, + context: &CheckContext, + given: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut elaborated = vec![]; + + for &constraint in given { + elaborated.push(constraint); + elaborate_superclasses(state, context, constraint, &mut elaborated)?; + } + + elaborated + .into_iter() + .map(|constraint| constraint_application(state, context, constraint)) + .filter_map_ok(|constraint| constraint) + .collect() +} + +/// Discovers superclass constraints for a given constraint. +pub fn elaborate_superclasses( + state: &mut CheckState, + context: &CheckContext, + constraint: TypeId, + constraints: &mut Vec, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + fn aux( + state: &mut CheckState, + context: &CheckContext, + constraint: TypeId, + constraints: &mut Vec, + seen: &mut HashSet<(FileId, TypeItemId)>, + ) -> QueryResult<()> + where + Q: ExternalQueries, + { + let Some(application) = constraint_application(state, context, constraint)? else { + return Ok(()); + }; + + if !seen.insert((application.file_id, application.item_id)) { + return Ok(()); + } + + let Some(class_info) = + toolkit::lookup_file_class(state, context, application.file_id, application.item_id)? + else { + return Ok(()); + }; + + if class_info.superclasses.is_empty() { + return Ok(()); + } + + let mut bindings = NameToType::default(); + for (binder_id, &argument) in + class_info.type_parameters.iter().zip(application.arguments.iter()) + { + let binder = context.lookup_forall_binder(*binder_id); + bindings.insert(binder.name, argument); + } + + for &superclass in &class_info.superclasses { + let substituted = SubstituteName::many(state, context, &bindings, superclass)?; + constraints.push(substituted); + aux(state, context, substituted, constraints, seen)?; + } + + Ok(()) + } + + let mut seen = HashSet::new(); + aux(state, context, constraint, constraints, &mut seen) +} + +/// Collects instance chains for a constraint from all eligible files. +fn collect_instance_chains( + state: &mut CheckState, + context: &CheckContext, + application: &ConstraintApplication, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let mut files_to_search = FxHashSet::default(); + files_to_search.insert(application.file_id); + + for &argument in &application.arguments { + CollectFileReferences::collect(state, context, argument, &mut files_to_search)?; + } + + let mut instances = vec![]; + + for &file_id in &files_to_search { + if file_id == context.id { + collect_instances_from_checked( + &mut instances, + &state.checked, + &context.indexed, + application.file_id, + application.item_id, + ); + } else { + let checked = context.queries.checked2(file_id)?; + let indexed = context.queries.indexed(file_id)?; + collect_instances_from_checked( + &mut instances, + &checked, + &indexed, + application.file_id, + application.item_id, + ); + } + } + + let mut grouped: FxHashMap> = FxHashMap::default(); + let mut singleton = vec![]; + + for instance in instances { + if let Some(chain_id) = instance.chain_id { + grouped.entry(chain_id).or_default().push(instance); + } else { + singleton.push(vec![instance]); + } + } + + let mut chains = singleton; + for (_, mut chain) in grouped { + chain.sort_by_key(|instance| instance.position); + chains.push(chain); + } + + Ok(chains) +} + +fn collect_instances_from_checked( + output: &mut Vec, + checked: &CheckedModule, + indexed: &indexing::IndexedModule, + class_file: FileId, + class_id: TypeItemId, +) { + output.extend( + checked + .instances + .iter() + .filter(|(_, instance)| instance.resolution == (class_file, class_id)) + .map(|(&id, checked)| CandidateInstance { + chain_id: indexed.pairs.instance_chain_id(id), + position: indexed.pairs.instance_chain_position(id).unwrap_or(0), + checked: checked.clone(), + }), + ); +} + +fn get_functional_dependencies( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + fn extract(type_item: Option<&lowering::TypeItemIr>) -> Vec { + let Some(lowering::TypeItemIr::ClassGroup { class: Some(class), .. }) = type_item else { + return vec![]; + }; + + class + .functional_dependencies + .iter() + .map(|functional_dependency| { + Fd::new( + functional_dependency.determiners.iter().map(|&x| x as usize), + functional_dependency.determined.iter().map(|&x| x as usize), + ) + }) + .collect() + } + + if file_id == context.id { + Ok(extract(context.lowered.info.get_type_item(item_id))) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(extract(lowered.info.get_type_item(item_id))) + } +} + +/// Determines if [`MatchType::Stuck`] arguments can be determined by functional dependencies. +fn can_determine_stuck( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, + match_results: &[MatchType], + stuck_positions: &[usize], +) -> QueryResult +where + Q: ExternalQueries, +{ + if stuck_positions.is_empty() { + return Ok(true); + } + + let functional_dependencies = get_functional_dependencies(context, file_id, item_id)?; + let initial: HashSet<_> = match_results + .iter() + .enumerate() + .filter_map(|(index, result)| matches!(result, MatchType::Match).then_some(index)) + .collect(); + + let determined = compute_closure(&functional_dependencies, &initial); + Ok(stuck_positions.iter().all(|index| determined.contains(index))) +} + +fn decompose_instance( + state: &mut CheckState, + context: &CheckContext, + instance: &CheckedInstance, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { binders, quantified } = + toolkit::inspect_quantified(state, context, instance.canonical)?; + + let mut current = quantified; + let mut constraints = vec![]; + + safe_loop! { + current = normalise::normalise(state, context, current)?; + match context.lookup_type(current) { + Type::Constrained(constraint, constrained) => { + constraints.push(constraint); + current = constrained; + } + _ => break, + } + } + + let Some(application) = constraint_application(state, context, current)? else { + return Ok(None); + }; + + if (application.file_id, application.item_id) != instance.resolution { + return Ok(None); + } + + Ok(Some(DecomposedInstance { binders, arguments: application.arguments, constraints })) +} + +/// Matches a wanted constraint to an instance. +fn match_instance( + state: &mut CheckState, + context: &CheckContext, + wanted: &ConstraintApplication, + instance: &CandidateInstance, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(decomposed) = decompose_instance(state, context, &instance.checked)? else { + return Ok(MatchInstance::Apart); + }; + + if wanted.arguments.len() != decomposed.arguments.len() { + return Ok(MatchInstance::Apart); + } + + let mut bindings = FxHashMap::default(); + let mut equalities = vec![]; + let mut match_results = vec![]; + let mut stuck_positions = vec![]; + + for (index, (&wanted_argument, &instance_argument)) in + wanted.arguments.iter().zip(decomposed.arguments.iter()).enumerate() + { + let match_result = match_type( + state, + context, + &mut bindings, + &mut equalities, + wanted_argument, + instance_argument, + )?; + + if matches!(match_result, MatchType::Apart) { + return Ok(MatchInstance::Apart); + } + + if matches!(match_result, MatchType::Stuck) { + stuck_positions.push(index); + } + + match_results.push(match_result); + } + + if !stuck_positions.is_empty() + && !can_determine_stuck( + context, + wanted.file_id, + wanted.item_id, + &match_results, + &stuck_positions, + )? + { + return Ok(MatchInstance::Stuck); + } + + for &index in &stuck_positions { + let substituted = + SubstituteName::many(state, context, &bindings, decomposed.arguments[index])?; + equalities.push((wanted.arguments[index], substituted)); + } + + for binder in &decomposed.binders { + if !bindings.contains_key(&binder.name) { + let unification = state.fresh_unification(context.queries, binder.kind); + bindings.insert(binder.name, unification); + } + } + + let constraints = decomposed + .constraints + .into_iter() + .map(|constraint| SubstituteName::many(state, context, &bindings, constraint)) + .collect::>>()?; + + Ok(MatchInstance::Match { constraints, equalities }) +} + +/// Matches a wanted constraint to given constraints. +fn match_given_instances( + state: &mut CheckState, + context: &CheckContext, + wanted: &ConstraintApplication, + given: &[ConstraintApplication], +) -> QueryResult> +where + Q: ExternalQueries, +{ + 'given: for given in given { + if wanted.file_id != given.file_id || wanted.item_id != given.item_id { + continue; + } + + if wanted.arguments.len() != given.arguments.len() { + continue; + } + + let mut stuck_positions = vec![]; + + for (index, (&wanted_argument, &given_argument)) in + wanted.arguments.iter().zip(given.arguments.iter()).enumerate() + { + let match_result = match_given_type(state, context, wanted_argument, given_argument)?; + + if matches!(match_result, MatchType::Apart) { + continue 'given; + } + + if matches!(match_result, MatchType::Stuck) { + stuck_positions.push(index); + } + } + + // Given constraints are valid by construction. When a unification + // variable makes a position stuck, it's safe to emit an equality + // rather than require functional dependencies to cover it. + let equalities = stuck_positions + .into_iter() + .map(|index| (wanted.arguments[index], given.arguments[index])); + return Ok(Some(MatchInstance::Match { + constraints: vec![], + equalities: equalities.collect(), + })); + } + + Ok(None) +} + +/// Matches an argument from a wanted constraint to one from an instance. +/// +/// This function emits substitutions and equalities when matching against +/// instances, for example: +/// +/// ```purescript +/// instance TypeEq a a True +/// ``` +/// +/// When matching the wanted constraint `TypeEq Int ?0` against this instance, +/// it creates the binding `a := Int`. This means that subsequent usages of `a` +/// are equal to `Int`, turning the second `match(a, ?0)` into `match(Int, ?0)`. +/// We use the [`can_unify`] function to speculate if these two types can be +/// unified, or if unifying them solves unification variables, encoded by the +/// [`CanUnify::Unify`] variant. +fn match_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &mut FxHashMap, + equalities: &mut Vec<(TypeId, TypeId)>, + wanted: TypeId, + given: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let wanted = normalise::normalise(state, context, wanted)?; + let given = normalise::normalise(state, context, given)?; + + if wanted == given { + return Ok(MatchType::Match); + } + + let wanted_t = context.lookup_type(wanted); + let given_t = context.lookup_type(given); + + match (wanted_t, given_t) { + (_, Type::Rigid(name, _, _)) => { + if let Some(&bound) = bindings.get(&name) { + match can_unify(state, context, wanted, bound)? { + CanUnify::Equal => Ok(MatchType::Match), + CanUnify::Apart => Ok(MatchType::Apart), + CanUnify::Unify => { + equalities.push((wanted, bound)); + Ok(MatchType::Match) + } + } + } else { + bindings.insert(name, wanted); + Ok(MatchType::Match) + } + } + + (Type::Unification(_), _) => Ok(MatchType::Stuck), + + (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { + let wanted_row = context.lookup_row_type(wanted_row_id); + let given_row = context.lookup_row_type(given_row_id); + match_row_type(state, context, bindings, equalities, wanted_row, given_row) + } + + (Type::Application(wf, wa), Type::Application(gf, ga)) => { + match_type(state, context, bindings, equalities, wf, gf)? + .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + } + + (Type::Function(wa, wr), Type::Function(ga, gr)) => { + match_type(state, context, bindings, equalities, wa, ga)? + .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) + } + + (Type::Function(wa, wr), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wa, wr); + match_type(state, context, bindings, equalities, wanted, given) + } + + (Type::Application(_, _), Type::Function(ga, gr)) => { + let given = context.intern_function_application(ga, gr); + match_type(state, context, bindings, equalities, wanted, given) + } + + (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { + match_type(state, context, bindings, equalities, wf, gf)? + .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + } + + (Type::Kinded(wi, wk), Type::Kinded(gi, gk)) => { + match_type(state, context, bindings, equalities, wi, gi)? + .and_then(|| match_type(state, context, bindings, equalities, wk, gk)) + } + + (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) + if wf == gf && wi == gi => + { + match_type(state, context, bindings, equalities, wl, gl)? + .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) + } + + (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { + let wsyn = context.lookup_synonym(wsyn); + let gsyn = context.lookup_synonym(gsyn); + + if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + return Ok(MatchType::Apart); + } + + wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( + MatchType::Match, + |result, (&wa, &ga)| { + result.and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + }, + ) + } + + _ => Ok(MatchType::Apart), + } +} + +/// Matches row types in instance heads. +/// +/// This function handles structural row matching for both the tail variable +/// form `( | r )` in determiner positions and labeled rows in determined +/// positions `( x :: T | r )`. This function partitions the two row types, +/// matches the shared fields, and handles the row tail. +fn match_row_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &mut FxHashMap, + equalities: &mut Vec<(TypeId, TypeId)>, + wanted_row: crate::core::RowType, + given_row: crate::core::RowType, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut wanted_only = vec![]; + let mut given_only = vec![]; + let mut result = MatchType::Match; + + for field in itertools::merge_join_by( + wanted_row.fields.iter(), + given_row.fields.iter(), + |wanted, given| wanted.label.cmp(&given.label), + ) { + match field { + itertools::EitherOrBoth::Both(wanted, given) => { + result = result.and_then(|| { + match_type(state, context, bindings, equalities, wanted.id, given.id) + })?; + // Given an open wanted row, additional fields from the + // given row can be absorbed into the wanted row's tail. + if matches!(result, MatchType::Apart) && wanted_row.tail.is_none() { + return Ok(MatchType::Apart); + } + } + itertools::EitherOrBoth::Left(wanted) => wanted_only.push(wanted), + itertools::EitherOrBoth::Right(given) => given_only.push(given), + } + } + + enum RowRest { + /// `( a :: Int )` and `( a :: Int | r )` + Additional, + /// `( | r )` + Open(TypeId), + /// `( )` + Closed, + } + + impl RowRest { + fn new(only: &[&crate::core::RowField], tail: Option) -> RowRest { + if !only.is_empty() { + RowRest::Additional + } else if let Some(tail) = tail { + RowRest::Open(tail) + } else { + RowRest::Closed + } + } + } + + use RowRest::*; + + let given_rest = RowRest::new(&given_only, given_row.tail); + let wanted_rest = RowRest::new(&wanted_only, wanted_row.tail); + + match given_rest { + // If there are additional given fields + Additional => match wanted_rest { + // we cannot match it against a tail-less wanted, + // nor against the additional wanted fields. + Closed | Additional => Ok(MatchType::Apart), + // we could potentially make progress by having the + // wanted tail absorb the additional given fields + Open(_) => Ok(MatchType::Stuck), + }, + // If the given row has a tail, match it against the + // additional fields and tail from the wanted row + Open(given_tail) => { + let fields = wanted_only.into_iter().cloned().collect_vec(); + let row = context.intern_row( + context.intern_row_type(crate::core::RowType::new(fields, wanted_row.tail)), + ); + result.and_then(|| match_type(state, context, bindings, equalities, row, given_tail)) + } + // If we have a closed given row + Closed => match wanted_rest { + // we cannot match it against fields in the wanted row + Additional => Ok(MatchType::Apart), + // we could make progress with an open wanted row + Open(_) => Ok(MatchType::Stuck), + // we can match it directly with a closed wanted row + Closed => Ok(result), + }, + } +} + +/// Matches an argument from a wanted constraint to one from a given constraint. +/// +/// This function is specialised for matching given constraints, like those +/// found in value signatures rather than top-level instance declarations; +/// unlike [`match_type`], this function does not build bindings or equalities +/// for rigid variables. +fn match_given_type( + state: &mut CheckState, + context: &CheckContext, + wanted: TypeId, + given: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let wanted = normalise::normalise(state, context, wanted)?; + let given = normalise::normalise(state, context, given)?; + + if wanted == given { + return Ok(MatchType::Match); + } + + let wanted_t = context.lookup_type(wanted); + let given_t = context.lookup_type(given); + + match (wanted_t, given_t) { + (Type::Unification(_), _) => Ok(MatchType::Stuck), + + (Type::Rigid(wname, _, wkind), Type::Rigid(gname, _, gkind)) => { + if wname == gname { + match_given_type(state, context, wkind, gkind) + } else { + Ok(MatchType::Apart) + } + } + + (Type::Application(wf, wa), Type::Application(gf, ga)) => { + match_given_type(state, context, wf, gf)? + .and_then(|| match_given_type(state, context, wa, ga)) + } + + (Type::Function(wa, wr), Type::Function(ga, gr)) => { + match_given_type(state, context, wa, ga)? + .and_then(|| match_given_type(state, context, wr, gr)) + } + + (Type::Function(wa, wr), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wa, wr); + match_given_type(state, context, wanted, given) + } + + (Type::Application(_, _), Type::Function(ga, gr)) => { + let given = context.intern_function_application(ga, gr); + match_given_type(state, context, wanted, given) + } + + (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { + let wanted_row = context.lookup_row_type(wanted_row_id); + let given_row = context.lookup_row_type(given_row_id); + + if wanted_row.fields.len() != given_row.fields.len() { + return Ok(MatchType::Apart); + } + + let mut result = MatchType::Match; + for (wanted_field, given_field) in wanted_row.fields.iter().zip(given_row.fields.iter()) + { + if wanted_field.label != given_field.label { + return Ok(MatchType::Apart); + } + result = result.and_then(|| { + match_given_type(state, context, wanted_field.id, given_field.id) + })?; + } + + match (wanted_row.tail, given_row.tail) { + (Some(wanted_tail), Some(given_tail)) => { + result.and_then(|| match_given_type(state, context, wanted_tail, given_tail)) + } + (Some(wanted_tail), None) => { + let wanted_tail = normalise::normalise(state, context, wanted_tail)?; + if matches!(context.lookup_type(wanted_tail), Type::Unification(_)) { + Ok(MatchType::Stuck) + } else { + Ok(MatchType::Apart) + } + } + (None, Some(given_tail)) => { + let given_tail = normalise::normalise(state, context, given_tail)?; + if matches!(context.lookup_type(given_tail), Type::Unification(_)) { + Ok(MatchType::Stuck) + } else { + Ok(MatchType::Apart) + } + } + (None, None) => Ok(result), + } + } + + (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { + match_given_type(state, context, wf, gf)? + .and_then(|| match_given_type(state, context, wa, ga)) + } + + (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) + if wf == gf && wi == gi => + { + match_given_type(state, context, wl, gl)? + .and_then(|| match_given_type(state, context, wr, gr)) + } + + (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { + let wsyn = context.lookup_synonym(wsyn); + let gsyn = context.lookup_synonym(gsyn); + + if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + return Ok(MatchType::Apart); + } + + wsyn.arguments + .iter() + .zip(gsyn.arguments.iter()) + .try_fold(MatchType::Match, |result, (&wa, &ga)| { + result.and_then(|| match_given_type(state, context, wa, ga)) + }) + } + + _ => Ok(MatchType::Apart), + } +} + +/// Determines if two types [`CanUnify`]. +/// +/// This is used in [`match_type`], where if two different types bind to the +/// same rigid variable, we determine if the types can actually unify before +/// generating an equality. This is effectively a pure version of the +/// [`unification::unify`] function. +fn can_unify( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(CanUnify::Equal); + } + + let t1_core = context.lookup_type(t1); + let t2_core = context.lookup_type(t2); + + match (t1_core, t2_core) { + (Type::Unification(_), _) | (_, Type::Unification(_)) => Ok(CanUnify::Unify), + (Type::Unknown(_), _) | (_, Type::Unknown(_)) => Ok(CanUnify::Unify), + (Type::Row(_), Type::Row(_)) => Ok(CanUnify::Unify), + + (Type::Application(f1, a1), Type::Application(f2, a2)) => { + can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) + } + (Type::Function(a1, r1), Type::Function(a2, r2)) => { + can_unify(state, context, a1, a2)?.and_then(|| can_unify(state, context, r1, r2)) + } + // Function(a, b) and Application(Application(f, a), b) can + // unify when `f` resolves to the Function constructor. + (Type::Function(..), Type::Application(..)) + | (Type::Application(..), Type::Function(..)) => Ok(CanUnify::Unify), + (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) => { + can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) + } + (Type::Kinded(i1, k1), Type::Kinded(i2, k2)) => { + can_unify(state, context, i1, i2)?.and_then(|| can_unify(state, context, k1, k2)) + } + (Type::Constrained(c1, i1), Type::Constrained(c2, i2)) => { + can_unify(state, context, c1, c2)?.and_then(|| can_unify(state, context, i1, i2)) + } + (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { + if n1 == n2 { + can_unify(state, context, k1, k2) + } else { + Ok(CanUnify::Apart) + } + } + (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { + let b1 = context.lookup_forall_binder(b1); + let b2 = context.lookup_forall_binder(b2); + can_unify(state, context, b1.kind, b2.kind)? + .and_then(|| can_unify(state, context, i1, i2)) + } + (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { + if f1 == f2 && i1 == i2 { + can_unify(state, context, l1, l2)?.and_then(|| can_unify(state, context, r1, r2)) + } else { + Ok(CanUnify::Apart) + } + } + (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { + let s1 = context.lookup_synonym(s1); + let s2 = context.lookup_synonym(s2); + if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { + return Ok(CanUnify::Apart); + } + + s1.arguments + .iter() + .zip(s2.arguments.iter()) + .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { + result.and_then(|| can_unify(state, context, a1, a2)) + }) + } + _ => Ok(CanUnify::Apart), + } +} + +/// Validates that all rows in instance declaration arguments +/// do not have labels in non-determined positions. +/// +/// In PureScript, instance declarations can only contain rows with labels +/// in positions that are determined by functional dependencies. In the +/// determiner position, only row variables such as `( | r )` are valid. +pub fn validate_instance_rows( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_item: TypeItemId, + arguments: &[TypeId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let functional_dependencies = get_functional_dependencies(context, class_file, class_item)?; + let all_determined = get_all_determined(&functional_dependencies); + + for (position, &argument_type) in arguments.iter().enumerate() { + if all_determined.contains(&position) { + continue; + } + + if HasLabeledRole::contains(state, context, argument_type)? { + let type_message = state.pretty_id(context, argument_type)?; + state.insert_error(ErrorKind::InstanceHeadLabeledRow { + class_file, + class_item, + position, + type_message, + }); + } + } + + Ok(()) +} + +struct CollectFileReferences<'a> { + files: &'a mut FxHashSet, +} + +impl<'a> CollectFileReferences<'a> { + fn collect( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + files: &'a mut FxHashSet, + ) -> QueryResult<()> + where + Q: ExternalQueries, + { + walk::walk_type(state, context, id, &mut CollectFileReferences { files }) + } +} + +impl TypeWalker for CollectFileReferences<'_> { + fn visit( + &mut self, + _state: &mut CheckState, + _context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Constructor(file_id, _) = t { + self.files.insert(*file_id); + } + Ok(WalkAction::Continue) + } +} + +struct HasLabeledRole { + contains: bool, +} + +impl HasLabeledRole { + fn contains( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + let mut walker = HasLabeledRole { contains: false }; + walk::walk_type(state, context, id, &mut walker)?; + Ok(walker.contains) + } +} + +impl TypeWalker for HasLabeledRole { + fn visit( + &mut self, + _state: &mut CheckState, + context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Row(row_id) = t { + let row = context.lookup_row_type(*row_id); + if !row.fields.is_empty() { + self.contains = true; + return Ok(WalkAction::Stop); + } + } + Ok(WalkAction::Continue) + } +} diff --git a/compiler-core/checking2/src/core/exhaustive/convert.rs b/compiler-core/checking2/src/core/exhaustive/convert.rs index 34684542..be1dd394 100644 --- a/compiler-core/checking2/src/core/exhaustive/convert.rs +++ b/compiler-core/checking2/src/core/exhaustive/convert.rs @@ -57,7 +57,7 @@ where } } lowering::BinderKind::Constructor { resolution, arguments } => { - convert_constructor_binder(state, context, &resolution, &arguments, t) + convert_constructor_binder(state, context, resolution, arguments, t) } lowering::BinderKind::Variable { .. } => Ok(state.allocate_wildcard(t)), lowering::BinderKind::Named { binder, .. } => match binder { @@ -84,8 +84,8 @@ where let constructor = PatternConstructor::Boolean(*boolean); Ok(state.allocate_constructor(constructor, t)) } - lowering::BinderKind::Array { array } => lower_array_binder(state, context, &array, t), - lowering::BinderKind::Record { record } => lower_record_binder(state, context, &record, t), + lowering::BinderKind::Array { array } => lower_array_binder(state, context, array, t), + lowering::BinderKind::Record { record } => lower_record_binder(state, context, record, t), lowering::BinderKind::Parenthesized { parenthesized } => match parenthesized { Some(id) => convert_binder(state, context, *id), None => Ok(state.allocate_wildcard(t)), @@ -167,7 +167,7 @@ where for element in record.iter() { match element { lowering::BinderRecordItem::RecordField { name, value } => { - let Some(name) = name.as_ref().map(SmolStr::clone) else { + let Some(name) = name.clone() else { continue; }; @@ -180,7 +180,7 @@ where provided_patterns.insert(name, pattern); } lowering::BinderRecordItem::RecordPun { id: _, name } => { - let Some(name) = name.as_ref().map(SmolStr::clone) else { + let Some(name) = name.clone() else { continue; }; diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index ecc7e4d3..a373260e 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -19,15 +19,16 @@ //! [rigid type variables]: crate::core::Type::Rigid use building_types::QueryResult; +use itertools::Itertools; use petgraph::algo; use petgraph::prelude::DiGraphMap; use rustc_hash::FxHashSet; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::walk::{self, TypeWalker, WalkAction}; -use crate::core::{ForallBinder, Name, Type, TypeId, normalise, zonk}; +use crate::core::{ForallBinder, Name, Type, TypeId, constraint, normalise, zonk}; use crate::state::{CheckState, UnificationEntry, UnificationState}; +use crate::{ExternalQueries, safe_loop}; type UniGraph = DiGraphMap; @@ -152,6 +153,19 @@ where Ok(unsolved) } +fn collect_unification( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut graph = UniGraph::new(); + collect_unification_into(&mut graph, state, context, id)?; + Ok(graph) +} + /// Generalise a type with the given unification variables. /// /// The `unsolved` parameter should be sourced from [`unsolved_unifications`]. @@ -237,6 +251,136 @@ where generalise_unsolved(state, context, id, &unsolved) } +#[derive(Default)] +pub struct ConstraintErrors { + pub ambiguous: Vec, + pub unsatisfied: Vec, +} + +pub fn insert_inferred_residuals( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, + residuals: Vec, + errors: &mut ConstraintErrors, +) -> QueryResult +where + Q: ExternalQueries, +{ + if residuals.is_empty() { + return Ok(type_id); + } + + let mut pending = vec![]; + let mut latent = vec![]; + + for residual in residuals { + let residual = zonk::zonk(state, context, residual)?; + + if residual == context.prim.partial { + latent.push(residual); + continue; + } + + let unification: FxHashSet = + collect_unification(state, context, residual)?.nodes().collect(); + + if unification.is_empty() { + errors.unsatisfied.push(residual); + } else { + pending.push((residual, unification)); + } + } + + let unifications = collect_unification(state, context, type_id)?.nodes().collect(); + let generalised = classify_constraints(pending, unifications, errors); + + let generalised = latent.into_iter().chain(generalised).sorted().collect_vec(); + let generalised = minimize_by_superclasses(state, context, generalised)?; + + let constrained = generalised + .into_iter() + .rfold(type_id, |inner, constraint| context.intern_constrained(constraint, inner)); + + Ok(constrained) +} + +fn minimize_by_superclasses( + state: &mut CheckState, + context: &CheckContext, + constraints: Vec, +) -> QueryResult> +where + Q: ExternalQueries, +{ + if constraints.len() <= 1 { + return Ok(constraints); + } + + let mut superclasses = FxHashSet::default(); + for &constraint in &constraints { + for application in superclass_applications(state, context, constraint)? { + superclasses.insert(application); + } + } + + Ok(constraints + .into_iter() + .filter(|&constraint| { + let application = + constraint::constraint_application(state, context, constraint).ok().flatten(); + application.is_none_or(|constraint| !superclasses.contains(&constraint)) + }) + .collect()) +} + +fn superclass_applications( + state: &mut CheckState, + context: &CheckContext, + constraint: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut superclasses = vec![]; + constraint::elaborate_superclasses(state, context, constraint, &mut superclasses)?; + superclasses + .into_iter() + .map(|constraint| constraint::constraint_application(state, context, constraint)) + .filter_map_ok(|constraint| constraint) + .collect() +} + +fn classify_constraints( + pending: Vec<(TypeId, FxHashSet)>, + unifications: FxHashSet, + errors: &mut ConstraintErrors, +) -> FxHashSet { + let mut reachable = unifications; + let mut valid = FxHashSet::default(); + let mut remaining = pending; + + safe_loop! { + let (connected, disconnected): (Vec<_>, Vec<_>) = + remaining.into_iter().partition(|(_, unification)| { + unification.iter().any(|variable| reachable.contains(variable)) + }); + + if connected.is_empty() { + break errors.ambiguous.extend(disconnected.into_iter().map(|(id, _)| id)); + } + + for (constraint, unification) in connected { + valid.insert(constraint); + reachable.extend(unification); + } + + remaining = disconnected; + } + + valid +} + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] enum ImplicitOrUnification { Implicit(Name, TypeId), diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 7fcd3e06..3c2f68a3 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -6,7 +6,9 @@ use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{CheckedSynonym, ForallBinder, Type, TypeId, normalise, unification}; +use crate::core::{ + CheckedClass, CheckedSynonym, ForallBinder, Type, TypeId, normalise, unification, +}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -20,6 +22,34 @@ pub struct InspectFunction { pub result: TypeId, } +pub fn extract_type_application( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult<(TypeId, Vec)> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Application(function, argument) => { + arguments.push(argument); + id = function; + } + Type::KindApplication(function, _) => { + id = function; + } + _ => break, + } + } + + arguments.reverse(); + Ok((id, arguments)) +} + pub fn lookup_file_type( state: &CheckState, context: &CheckContext, @@ -88,6 +118,23 @@ where } } +pub fn lookup_file_class( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + if file_id == context.id { + Ok(state.checked.lookup_class(item_id)) + } else { + let checked = context.queries.checked2(file_id)?; + Ok(checked.lookup_class(item_id)) + } +} + pub fn lookup_file_synonym( state: &CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 94f2f344..5eb9466e 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -4,10 +4,13 @@ use building_types::QueryResult; use files::FileId; use indexing::{TermItemId, TermItemKind, TypeItemId}; use lowering::TermItemIr; +use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{CheckedInstance, Type, TypeId, generalise, toolkit, unification, zonk}; +use crate::core::{ + CheckedInstance, Type, TypeId, constraint, generalise, toolkit, unification, zonk, +}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::terms::equations; use crate::source::types; @@ -16,6 +19,12 @@ use crate::state::CheckState; #[derive(Default)] struct TermSccState { operator: Vec, + value_groups: FxHashMap, +} + +enum PendingValueGroup { + Checked { residuals: Vec }, + Inferred { residuals: Vec }, } pub fn check_term_items(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> @@ -38,9 +47,7 @@ where let items = scc.as_slice(); let items = items.iter().filter_map(|&item_id| { - let Some(item) = context.lowered.info.get_term_item(item_id) else { - return None; - }; + let item = context.lowered.info.get_term_item(item_id)?; let TermItemIr::Instance { constraints, resolution, arguments, .. } = item else { return None; }; @@ -102,10 +109,16 @@ where let mut class_type = context.queries.intern_type(Type::Constructor(class_file, class_id)); let mut class_kind = class_kind; + let mut checked_arguments = Vec::with_capacity(arguments.len()); for &argument in arguments { (class_type, class_kind) = types::infer_application_kind(state, context, (class_type, class_kind), argument)?; + let (_, extracted_arguments) = + toolkit::extract_type_application(state, context, class_type)?; + if let Some(&checked_argument) = extracted_arguments.last() { + checked_arguments.push(checked_argument); + } } unification::subtype(state, context, class_kind, context.prim.constraint)?; @@ -122,6 +135,8 @@ where canonical = context.intern_constrained(constraint, canonical); } + constraint::validate_instance_rows(state, context, class_file, class_id, &checked_arguments)?; + let resolution = (class_file, class_id); let canonical = zonk::zonk(state, context, canonical)?; let canonical = generalise::generalise_implicit(state, context, canonical)?; @@ -143,7 +158,7 @@ where } if scc.is_recursive() { - prepare_binding_group(state, context, &items); + prepare_binding_group(state, context, items); } let mut term_scc = TermSccState::default(); @@ -152,7 +167,7 @@ where check_term_equation(state, context, &mut term_scc, item)?; } - finalise_term_binding_group(state, context, items)?; + finalise_term_binding_group(state, context, &mut term_scc, items)?; finalise_term_operators(state, context, &mut term_scc)?; } @@ -231,7 +246,12 @@ where check_term_operator(state, context, scc, item_id, *resolution)?; } TermItemIr::ValueGroup { signature, equations } => { - check_value_group(state, context, item_id, *signature, equations)?; + let pending = state.with_implication(|state| { + check_value_group(state, context, item_id, *signature, equations) + })?; + if let Some(pending) = pending { + scc.value_groups.insert(item_id, pending); + } } _ => (), } @@ -245,7 +265,7 @@ fn check_value_group( item_id: TermItemId, signature: Option, equations: &[lowering::Equation], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -260,19 +280,20 @@ fn check_value_group_core( item_id: TermItemId, signature: Option, equations: &[lowering::Equation], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { if let Some(signature_id) = signature && let Some(signature_type) = state.checked.lookup_term(item_id) { - check_value_group_core_check(state, context, signature_id, signature_type, equations)?; + let residuals = + check_value_group_core_check(state, context, signature_id, signature_type, equations)?; + Ok(Some(PendingValueGroup::Checked { residuals })) } else { - check_value_group_core_infer(state, context, item_id, equations)?; + let residuals = check_value_group_core_infer(state, context, item_id, equations)?; + Ok(Some(PendingValueGroup::Inferred { residuals })) } - - Ok(()) } fn check_value_group_core_check( @@ -281,7 +302,7 @@ fn check_value_group_core_check( signature_id: lowering::TypeId, signature_type: TypeId, equations: &[lowering::Equation], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -303,7 +324,7 @@ where equations, )?; - Ok(()) + state.solve_constraints(context) } fn check_value_group_core_infer( @@ -311,7 +332,7 @@ fn check_value_group_core_infer( context: &CheckContext, item_id: TermItemId, equations: &[lowering::Equation], -) -> QueryResult<()> +) -> QueryResult> where Q: ExternalQueries, { @@ -319,33 +340,67 @@ where state.checked.terms.insert(item_id, group_type); equations::infer_equations_core(state, context, group_type, equations)?; - Ok(()) + state.solve_constraints(context) } fn finalise_term_binding_group( state: &mut CheckState, context: &CheckContext, + scc: &mut TermSccState, items: &[TermItemId], ) -> QueryResult<()> where Q: ExternalQueries, { - let mut pending = Vec::with_capacity(items.len()); + struct Pending { + t: TypeId, + unsolved: Vec, + errors: generalise::ConstraintErrors, + } + + let mut pending = vec![]; for &item_id in items { - let Some(kind) = state.checked.terms.get(&item_id).copied() else { + let Some(t) = state.checked.terms.get(&item_id).copied() else { continue; }; - let kind = zonk::zonk(state, context, kind)?; - let unsolved = generalise::unsolved_unifications(state, context, kind)?; + let group = scc.value_groups.remove(&item_id); + let t = zonk::zonk(state, context, t)?; + + let mut errors = generalise::ConstraintErrors::default(); - pending.push((item_id, kind, unsolved)); + let t = match group { + Some(PendingValueGroup::Checked { residuals }) => { + errors.unsatisfied.extend(residuals); + t + } + Some(PendingValueGroup::Inferred { residuals }) => { + generalise::insert_inferred_residuals(state, context, t, residuals, &mut errors)? + } + None => t, + }; + + let t = zonk::zonk(state, context, t)?; + let unsolved = generalise::unsolved_unifications(state, context, t)?; + + pending.push((item_id, Pending { t, unsolved, errors })); } - for (item_id, kind, unsolved) in pending { - let kind = generalise::generalise_unsolved(state, context, kind, &unsolved)?; - state.checked.terms.insert(item_id, kind); + for (item_id, Pending { t, unsolved, errors }) in pending { + let t = generalise::generalise_unsolved(state, context, t, &unsolved)?; + state.checked.terms.insert(item_id, t); + + for constraint in errors.ambiguous { + let constraint = zonk::zonk(state, context, constraint)?; + let constraint = state.pretty_id(context, constraint)?; + state.insert_error(ErrorKind::AmbiguousConstraint { constraint }); + } + for constraint in errors.unsatisfied { + let constraint = zonk::zonk(state, context, constraint)?; + let constraint = state.pretty_id(context, constraint)?; + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } } Ok(()) diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 38fef314..857f075a 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -11,7 +11,7 @@ use crate::context::CheckContext; use crate::core::exhaustive::{ ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternInterner, PatternKind, }; -use crate::core::{Depth, Name, SmolStrId, Type, TypeId, pretty, zonk}; +use crate::core::{Depth, Name, SmolStrId, Type, TypeId, constraint, pretty, zonk}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; use crate::{CheckedModule, ExternalQueries}; @@ -199,6 +199,13 @@ impl CheckState { result } + pub fn solve_constraints(&mut self, context: &CheckContext) -> QueryResult> + where + Q: ExternalQueries, + { + constraint::solve_implication(self, context) + } + pub fn pretty_id(&mut self, context: &CheckContext, id: TypeId) -> QueryResult where Q: ExternalQueries, diff --git a/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap index d03c6fb4..aee46711 100644 --- a/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap +++ b/tests-integration/fixtures/checking2/030_exhaustive_case_infer/Main.snap @@ -6,7 +6,7 @@ expression: report Terms Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) Nothing :: forall (a :: Type). Maybe (a :: Type) -test :: forall (t1 :: Type). Maybe (t1 :: Type) -> Int +test :: forall (t1 :: Type). Partial => Maybe (t1 :: Type) -> Int Types Maybe :: Type -> Type diff --git a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap index 3c588e95..c3bdc674 100644 --- a/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap +++ b/tests-integration/fixtures/checking2/033_exhaustive_guards_otherwise/Main.snap @@ -6,8 +6,8 @@ expression: report Terms Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) Nothing :: forall (a :: Type). Maybe (a :: Type) -test1 :: forall (t1 :: Type). Maybe (t1 :: Type) -> Int -test2 :: forall (t2 :: Type). Maybe (t2 :: Type) -> Int +test1 :: forall (t1 :: Type). Partial => Maybe (t1 :: Type) -> Int +test2 :: forall (t2 :: Type). Partial => Maybe (t2 :: Type) -> Int Types Maybe :: Type -> Type diff --git a/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap index db613c2e..d89ac40d 100644 --- a/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap +++ b/tests-integration/fixtures/checking2/034_exhaustive_let_pattern/Main.snap @@ -6,7 +6,7 @@ expression: report Terms Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) Nothing :: forall (a :: Type). Maybe (a :: Type) -test :: Int +test :: Partial => Int Types Maybe :: Type -> Type From ee799d57f752e9f2de6acad4e98e489e0d834569 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 22:55:48 +0800 Subject: [PATCH 284/386] Organise constraint solving into submodules --- .../checking2/src/core/constraint.rs | 922 +----------------- .../checking2/src/core/constraint/given.rs | 217 +++++ .../src/core/constraint/instances.rs | 717 ++++++++++++++ .../checking2/src/source/term_items.rs | 2 +- 4 files changed, 943 insertions(+), 915 deletions(-) create mode 100644 compiler-core/checking2/src/core/constraint/given.rs create mode 100644 compiler-core/checking2/src/core/constraint/instances.rs diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index 47f5836e..01d95eb3 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -1,26 +1,24 @@ pub mod functional_dependency; +pub mod given; +pub mod instances; use std::collections::{HashSet, VecDeque}; use std::mem; use building_types::QueryResult; use files::FileId; -use indexing::{InstanceChainId, TypeItemId}; +use indexing::TypeItemId; use itertools::Itertools; -use rustc_hash::{FxHashMap, FxHashSet}; +use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::{NameToType, SubstituteName}; -use crate::core::walk::{self, TypeWalker, WalkAction}; -use crate::core::{ - CheckedInstance, ForallBinder, Name, Type, TypeId, normalise, toolkit, unification, -}; -use crate::error::ErrorKind; +use crate::core::{Type, TypeId, normalise, toolkit, unification}; use crate::implication::ImplicationId; use crate::state::CheckState; -use crate::{CheckedModule, ExternalQueries, safe_loop}; -use functional_dependency::{Fd, compute_closure, get_all_determined}; +use given::{elaborate_given, match_given_instances}; +use instances::{collect_instance_chains, match_instance}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ConstraintApplication { @@ -30,7 +28,7 @@ pub struct ConstraintApplication { } #[derive(Debug, Clone)] -enum MatchInstance { +pub enum MatchInstance { Match { constraints: Vec, equalities: Vec<(TypeId, TypeId)> }, Apart, Stuck, @@ -49,32 +47,6 @@ impl MatchType { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CanUnify { - Equal, - Apart, - Unify, -} - -impl CanUnify { - fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { - if let CanUnify::Equal = self { f() } else { Ok(self) } - } -} - -#[derive(Clone)] -struct CandidateInstance { - chain_id: Option, - position: u32, - checked: CheckedInstance, -} - -struct DecomposedInstance { - binders: Vec, - arguments: Vec, - constraints: Vec, -} - pub fn solve_implication( state: &mut CheckState, context: &CheckContext, @@ -208,29 +180,6 @@ where }) } -/// Discovers implied constraints from given constraints. -fn elaborate_given( - state: &mut CheckState, - context: &CheckContext, - given: &[TypeId], -) -> QueryResult> -where - Q: ExternalQueries, -{ - let mut elaborated = vec![]; - - for &constraint in given { - elaborated.push(constraint); - elaborate_superclasses(state, context, constraint, &mut elaborated)?; - } - - elaborated - .into_iter() - .map(|constraint| constraint_application(state, context, constraint)) - .filter_map_ok(|constraint| constraint) - .collect() -} - /// Discovers superclass constraints for a given constraint. pub fn elaborate_superclasses( state: &mut CheckState, @@ -289,858 +238,3 @@ where let mut seen = HashSet::new(); aux(state, context, constraint, constraints, &mut seen) } - -/// Collects instance chains for a constraint from all eligible files. -fn collect_instance_chains( - state: &mut CheckState, - context: &CheckContext, - application: &ConstraintApplication, -) -> QueryResult>> -where - Q: ExternalQueries, -{ - let mut files_to_search = FxHashSet::default(); - files_to_search.insert(application.file_id); - - for &argument in &application.arguments { - CollectFileReferences::collect(state, context, argument, &mut files_to_search)?; - } - - let mut instances = vec![]; - - for &file_id in &files_to_search { - if file_id == context.id { - collect_instances_from_checked( - &mut instances, - &state.checked, - &context.indexed, - application.file_id, - application.item_id, - ); - } else { - let checked = context.queries.checked2(file_id)?; - let indexed = context.queries.indexed(file_id)?; - collect_instances_from_checked( - &mut instances, - &checked, - &indexed, - application.file_id, - application.item_id, - ); - } - } - - let mut grouped: FxHashMap> = FxHashMap::default(); - let mut singleton = vec![]; - - for instance in instances { - if let Some(chain_id) = instance.chain_id { - grouped.entry(chain_id).or_default().push(instance); - } else { - singleton.push(vec![instance]); - } - } - - let mut chains = singleton; - for (_, mut chain) in grouped { - chain.sort_by_key(|instance| instance.position); - chains.push(chain); - } - - Ok(chains) -} - -fn collect_instances_from_checked( - output: &mut Vec, - checked: &CheckedModule, - indexed: &indexing::IndexedModule, - class_file: FileId, - class_id: TypeItemId, -) { - output.extend( - checked - .instances - .iter() - .filter(|(_, instance)| instance.resolution == (class_file, class_id)) - .map(|(&id, checked)| CandidateInstance { - chain_id: indexed.pairs.instance_chain_id(id), - position: indexed.pairs.instance_chain_position(id).unwrap_or(0), - checked: checked.clone(), - }), - ); -} - -fn get_functional_dependencies( - context: &CheckContext, - file_id: FileId, - item_id: TypeItemId, -) -> QueryResult> -where - Q: ExternalQueries, -{ - fn extract(type_item: Option<&lowering::TypeItemIr>) -> Vec { - let Some(lowering::TypeItemIr::ClassGroup { class: Some(class), .. }) = type_item else { - return vec![]; - }; - - class - .functional_dependencies - .iter() - .map(|functional_dependency| { - Fd::new( - functional_dependency.determiners.iter().map(|&x| x as usize), - functional_dependency.determined.iter().map(|&x| x as usize), - ) - }) - .collect() - } - - if file_id == context.id { - Ok(extract(context.lowered.info.get_type_item(item_id))) - } else { - let lowered = context.queries.lowered(file_id)?; - Ok(extract(lowered.info.get_type_item(item_id))) - } -} - -/// Determines if [`MatchType::Stuck`] arguments can be determined by functional dependencies. -fn can_determine_stuck( - context: &CheckContext, - file_id: FileId, - item_id: TypeItemId, - match_results: &[MatchType], - stuck_positions: &[usize], -) -> QueryResult -where - Q: ExternalQueries, -{ - if stuck_positions.is_empty() { - return Ok(true); - } - - let functional_dependencies = get_functional_dependencies(context, file_id, item_id)?; - let initial: HashSet<_> = match_results - .iter() - .enumerate() - .filter_map(|(index, result)| matches!(result, MatchType::Match).then_some(index)) - .collect(); - - let determined = compute_closure(&functional_dependencies, &initial); - Ok(stuck_positions.iter().all(|index| determined.contains(index))) -} - -fn decompose_instance( - state: &mut CheckState, - context: &CheckContext, - instance: &CheckedInstance, -) -> QueryResult> -where - Q: ExternalQueries, -{ - let toolkit::InspectQuantified { binders, quantified } = - toolkit::inspect_quantified(state, context, instance.canonical)?; - - let mut current = quantified; - let mut constraints = vec![]; - - safe_loop! { - current = normalise::normalise(state, context, current)?; - match context.lookup_type(current) { - Type::Constrained(constraint, constrained) => { - constraints.push(constraint); - current = constrained; - } - _ => break, - } - } - - let Some(application) = constraint_application(state, context, current)? else { - return Ok(None); - }; - - if (application.file_id, application.item_id) != instance.resolution { - return Ok(None); - } - - Ok(Some(DecomposedInstance { binders, arguments: application.arguments, constraints })) -} - -/// Matches a wanted constraint to an instance. -fn match_instance( - state: &mut CheckState, - context: &CheckContext, - wanted: &ConstraintApplication, - instance: &CandidateInstance, -) -> QueryResult -where - Q: ExternalQueries, -{ - let Some(decomposed) = decompose_instance(state, context, &instance.checked)? else { - return Ok(MatchInstance::Apart); - }; - - if wanted.arguments.len() != decomposed.arguments.len() { - return Ok(MatchInstance::Apart); - } - - let mut bindings = FxHashMap::default(); - let mut equalities = vec![]; - let mut match_results = vec![]; - let mut stuck_positions = vec![]; - - for (index, (&wanted_argument, &instance_argument)) in - wanted.arguments.iter().zip(decomposed.arguments.iter()).enumerate() - { - let match_result = match_type( - state, - context, - &mut bindings, - &mut equalities, - wanted_argument, - instance_argument, - )?; - - if matches!(match_result, MatchType::Apart) { - return Ok(MatchInstance::Apart); - } - - if matches!(match_result, MatchType::Stuck) { - stuck_positions.push(index); - } - - match_results.push(match_result); - } - - if !stuck_positions.is_empty() - && !can_determine_stuck( - context, - wanted.file_id, - wanted.item_id, - &match_results, - &stuck_positions, - )? - { - return Ok(MatchInstance::Stuck); - } - - for &index in &stuck_positions { - let substituted = - SubstituteName::many(state, context, &bindings, decomposed.arguments[index])?; - equalities.push((wanted.arguments[index], substituted)); - } - - for binder in &decomposed.binders { - if !bindings.contains_key(&binder.name) { - let unification = state.fresh_unification(context.queries, binder.kind); - bindings.insert(binder.name, unification); - } - } - - let constraints = decomposed - .constraints - .into_iter() - .map(|constraint| SubstituteName::many(state, context, &bindings, constraint)) - .collect::>>()?; - - Ok(MatchInstance::Match { constraints, equalities }) -} - -/// Matches a wanted constraint to given constraints. -fn match_given_instances( - state: &mut CheckState, - context: &CheckContext, - wanted: &ConstraintApplication, - given: &[ConstraintApplication], -) -> QueryResult> -where - Q: ExternalQueries, -{ - 'given: for given in given { - if wanted.file_id != given.file_id || wanted.item_id != given.item_id { - continue; - } - - if wanted.arguments.len() != given.arguments.len() { - continue; - } - - let mut stuck_positions = vec![]; - - for (index, (&wanted_argument, &given_argument)) in - wanted.arguments.iter().zip(given.arguments.iter()).enumerate() - { - let match_result = match_given_type(state, context, wanted_argument, given_argument)?; - - if matches!(match_result, MatchType::Apart) { - continue 'given; - } - - if matches!(match_result, MatchType::Stuck) { - stuck_positions.push(index); - } - } - - // Given constraints are valid by construction. When a unification - // variable makes a position stuck, it's safe to emit an equality - // rather than require functional dependencies to cover it. - let equalities = stuck_positions - .into_iter() - .map(|index| (wanted.arguments[index], given.arguments[index])); - return Ok(Some(MatchInstance::Match { - constraints: vec![], - equalities: equalities.collect(), - })); - } - - Ok(None) -} - -/// Matches an argument from a wanted constraint to one from an instance. -/// -/// This function emits substitutions and equalities when matching against -/// instances, for example: -/// -/// ```purescript -/// instance TypeEq a a True -/// ``` -/// -/// When matching the wanted constraint `TypeEq Int ?0` against this instance, -/// it creates the binding `a := Int`. This means that subsequent usages of `a` -/// are equal to `Int`, turning the second `match(a, ?0)` into `match(Int, ?0)`. -/// We use the [`can_unify`] function to speculate if these two types can be -/// unified, or if unifying them solves unification variables, encoded by the -/// [`CanUnify::Unify`] variant. -fn match_type( - state: &mut CheckState, - context: &CheckContext, - bindings: &mut FxHashMap, - equalities: &mut Vec<(TypeId, TypeId)>, - wanted: TypeId, - given: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let wanted = normalise::normalise(state, context, wanted)?; - let given = normalise::normalise(state, context, given)?; - - if wanted == given { - return Ok(MatchType::Match); - } - - let wanted_t = context.lookup_type(wanted); - let given_t = context.lookup_type(given); - - match (wanted_t, given_t) { - (_, Type::Rigid(name, _, _)) => { - if let Some(&bound) = bindings.get(&name) { - match can_unify(state, context, wanted, bound)? { - CanUnify::Equal => Ok(MatchType::Match), - CanUnify::Apart => Ok(MatchType::Apart), - CanUnify::Unify => { - equalities.push((wanted, bound)); - Ok(MatchType::Match) - } - } - } else { - bindings.insert(name, wanted); - Ok(MatchType::Match) - } - } - - (Type::Unification(_), _) => Ok(MatchType::Stuck), - - (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { - let wanted_row = context.lookup_row_type(wanted_row_id); - let given_row = context.lookup_row_type(given_row_id); - match_row_type(state, context, bindings, equalities, wanted_row, given_row) - } - - (Type::Application(wf, wa), Type::Application(gf, ga)) => { - match_type(state, context, bindings, equalities, wf, gf)? - .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) - } - - (Type::Function(wa, wr), Type::Function(ga, gr)) => { - match_type(state, context, bindings, equalities, wa, ga)? - .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) - } - - (Type::Function(wa, wr), Type::Application(_, _)) => { - let wanted = context.intern_function_application(wa, wr); - match_type(state, context, bindings, equalities, wanted, given) - } - - (Type::Application(_, _), Type::Function(ga, gr)) => { - let given = context.intern_function_application(ga, gr); - match_type(state, context, bindings, equalities, wanted, given) - } - - (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { - match_type(state, context, bindings, equalities, wf, gf)? - .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) - } - - (Type::Kinded(wi, wk), Type::Kinded(gi, gk)) => { - match_type(state, context, bindings, equalities, wi, gi)? - .and_then(|| match_type(state, context, bindings, equalities, wk, gk)) - } - - (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) - if wf == gf && wi == gi => - { - match_type(state, context, bindings, equalities, wl, gl)? - .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) - } - - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { - let wsyn = context.lookup_synonym(wsyn); - let gsyn = context.lookup_synonym(gsyn); - - if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { - return Ok(MatchType::Apart); - } - - wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( - MatchType::Match, - |result, (&wa, &ga)| { - result.and_then(|| match_type(state, context, bindings, equalities, wa, ga)) - }, - ) - } - - _ => Ok(MatchType::Apart), - } -} - -/// Matches row types in instance heads. -/// -/// This function handles structural row matching for both the tail variable -/// form `( | r )` in determiner positions and labeled rows in determined -/// positions `( x :: T | r )`. This function partitions the two row types, -/// matches the shared fields, and handles the row tail. -fn match_row_type( - state: &mut CheckState, - context: &CheckContext, - bindings: &mut FxHashMap, - equalities: &mut Vec<(TypeId, TypeId)>, - wanted_row: crate::core::RowType, - given_row: crate::core::RowType, -) -> QueryResult -where - Q: ExternalQueries, -{ - let mut wanted_only = vec![]; - let mut given_only = vec![]; - let mut result = MatchType::Match; - - for field in itertools::merge_join_by( - wanted_row.fields.iter(), - given_row.fields.iter(), - |wanted, given| wanted.label.cmp(&given.label), - ) { - match field { - itertools::EitherOrBoth::Both(wanted, given) => { - result = result.and_then(|| { - match_type(state, context, bindings, equalities, wanted.id, given.id) - })?; - // Given an open wanted row, additional fields from the - // given row can be absorbed into the wanted row's tail. - if matches!(result, MatchType::Apart) && wanted_row.tail.is_none() { - return Ok(MatchType::Apart); - } - } - itertools::EitherOrBoth::Left(wanted) => wanted_only.push(wanted), - itertools::EitherOrBoth::Right(given) => given_only.push(given), - } - } - - enum RowRest { - /// `( a :: Int )` and `( a :: Int | r )` - Additional, - /// `( | r )` - Open(TypeId), - /// `( )` - Closed, - } - - impl RowRest { - fn new(only: &[&crate::core::RowField], tail: Option) -> RowRest { - if !only.is_empty() { - RowRest::Additional - } else if let Some(tail) = tail { - RowRest::Open(tail) - } else { - RowRest::Closed - } - } - } - - use RowRest::*; - - let given_rest = RowRest::new(&given_only, given_row.tail); - let wanted_rest = RowRest::new(&wanted_only, wanted_row.tail); - - match given_rest { - // If there are additional given fields - Additional => match wanted_rest { - // we cannot match it against a tail-less wanted, - // nor against the additional wanted fields. - Closed | Additional => Ok(MatchType::Apart), - // we could potentially make progress by having the - // wanted tail absorb the additional given fields - Open(_) => Ok(MatchType::Stuck), - }, - // If the given row has a tail, match it against the - // additional fields and tail from the wanted row - Open(given_tail) => { - let fields = wanted_only.into_iter().cloned().collect_vec(); - let row = context.intern_row( - context.intern_row_type(crate::core::RowType::new(fields, wanted_row.tail)), - ); - result.and_then(|| match_type(state, context, bindings, equalities, row, given_tail)) - } - // If we have a closed given row - Closed => match wanted_rest { - // we cannot match it against fields in the wanted row - Additional => Ok(MatchType::Apart), - // we could make progress with an open wanted row - Open(_) => Ok(MatchType::Stuck), - // we can match it directly with a closed wanted row - Closed => Ok(result), - }, - } -} - -/// Matches an argument from a wanted constraint to one from a given constraint. -/// -/// This function is specialised for matching given constraints, like those -/// found in value signatures rather than top-level instance declarations; -/// unlike [`match_type`], this function does not build bindings or equalities -/// for rigid variables. -fn match_given_type( - state: &mut CheckState, - context: &CheckContext, - wanted: TypeId, - given: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let wanted = normalise::normalise(state, context, wanted)?; - let given = normalise::normalise(state, context, given)?; - - if wanted == given { - return Ok(MatchType::Match); - } - - let wanted_t = context.lookup_type(wanted); - let given_t = context.lookup_type(given); - - match (wanted_t, given_t) { - (Type::Unification(_), _) => Ok(MatchType::Stuck), - - (Type::Rigid(wname, _, wkind), Type::Rigid(gname, _, gkind)) => { - if wname == gname { - match_given_type(state, context, wkind, gkind) - } else { - Ok(MatchType::Apart) - } - } - - (Type::Application(wf, wa), Type::Application(gf, ga)) => { - match_given_type(state, context, wf, gf)? - .and_then(|| match_given_type(state, context, wa, ga)) - } - - (Type::Function(wa, wr), Type::Function(ga, gr)) => { - match_given_type(state, context, wa, ga)? - .and_then(|| match_given_type(state, context, wr, gr)) - } - - (Type::Function(wa, wr), Type::Application(_, _)) => { - let wanted = context.intern_function_application(wa, wr); - match_given_type(state, context, wanted, given) - } - - (Type::Application(_, _), Type::Function(ga, gr)) => { - let given = context.intern_function_application(ga, gr); - match_given_type(state, context, wanted, given) - } - - (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { - let wanted_row = context.lookup_row_type(wanted_row_id); - let given_row = context.lookup_row_type(given_row_id); - - if wanted_row.fields.len() != given_row.fields.len() { - return Ok(MatchType::Apart); - } - - let mut result = MatchType::Match; - for (wanted_field, given_field) in wanted_row.fields.iter().zip(given_row.fields.iter()) - { - if wanted_field.label != given_field.label { - return Ok(MatchType::Apart); - } - result = result.and_then(|| { - match_given_type(state, context, wanted_field.id, given_field.id) - })?; - } - - match (wanted_row.tail, given_row.tail) { - (Some(wanted_tail), Some(given_tail)) => { - result.and_then(|| match_given_type(state, context, wanted_tail, given_tail)) - } - (Some(wanted_tail), None) => { - let wanted_tail = normalise::normalise(state, context, wanted_tail)?; - if matches!(context.lookup_type(wanted_tail), Type::Unification(_)) { - Ok(MatchType::Stuck) - } else { - Ok(MatchType::Apart) - } - } - (None, Some(given_tail)) => { - let given_tail = normalise::normalise(state, context, given_tail)?; - if matches!(context.lookup_type(given_tail), Type::Unification(_)) { - Ok(MatchType::Stuck) - } else { - Ok(MatchType::Apart) - } - } - (None, None) => Ok(result), - } - } - - (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { - match_given_type(state, context, wf, gf)? - .and_then(|| match_given_type(state, context, wa, ga)) - } - - (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) - if wf == gf && wi == gi => - { - match_given_type(state, context, wl, gl)? - .and_then(|| match_given_type(state, context, wr, gr)) - } - - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { - let wsyn = context.lookup_synonym(wsyn); - let gsyn = context.lookup_synonym(gsyn); - - if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { - return Ok(MatchType::Apart); - } - - wsyn.arguments - .iter() - .zip(gsyn.arguments.iter()) - .try_fold(MatchType::Match, |result, (&wa, &ga)| { - result.and_then(|| match_given_type(state, context, wa, ga)) - }) - } - - _ => Ok(MatchType::Apart), - } -} - -/// Determines if two types [`CanUnify`]. -/// -/// This is used in [`match_type`], where if two different types bind to the -/// same rigid variable, we determine if the types can actually unify before -/// generating an equality. This is effectively a pure version of the -/// [`unification::unify`] function. -fn can_unify( - state: &mut CheckState, - context: &CheckContext, - t1: TypeId, - t2: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; - - if t1 == t2 { - return Ok(CanUnify::Equal); - } - - let t1_core = context.lookup_type(t1); - let t2_core = context.lookup_type(t2); - - match (t1_core, t2_core) { - (Type::Unification(_), _) | (_, Type::Unification(_)) => Ok(CanUnify::Unify), - (Type::Unknown(_), _) | (_, Type::Unknown(_)) => Ok(CanUnify::Unify), - (Type::Row(_), Type::Row(_)) => Ok(CanUnify::Unify), - - (Type::Application(f1, a1), Type::Application(f2, a2)) => { - can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) - } - (Type::Function(a1, r1), Type::Function(a2, r2)) => { - can_unify(state, context, a1, a2)?.and_then(|| can_unify(state, context, r1, r2)) - } - // Function(a, b) and Application(Application(f, a), b) can - // unify when `f` resolves to the Function constructor. - (Type::Function(..), Type::Application(..)) - | (Type::Application(..), Type::Function(..)) => Ok(CanUnify::Unify), - (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) => { - can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) - } - (Type::Kinded(i1, k1), Type::Kinded(i2, k2)) => { - can_unify(state, context, i1, i2)?.and_then(|| can_unify(state, context, k1, k2)) - } - (Type::Constrained(c1, i1), Type::Constrained(c2, i2)) => { - can_unify(state, context, c1, c2)?.and_then(|| can_unify(state, context, i1, i2)) - } - (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { - if n1 == n2 { - can_unify(state, context, k1, k2) - } else { - Ok(CanUnify::Apart) - } - } - (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { - let b1 = context.lookup_forall_binder(b1); - let b2 = context.lookup_forall_binder(b2); - can_unify(state, context, b1.kind, b2.kind)? - .and_then(|| can_unify(state, context, i1, i2)) - } - (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { - if f1 == f2 && i1 == i2 { - can_unify(state, context, l1, l2)?.and_then(|| can_unify(state, context, r1, r2)) - } else { - Ok(CanUnify::Apart) - } - } - (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { - let s1 = context.lookup_synonym(s1); - let s2 = context.lookup_synonym(s2); - if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { - return Ok(CanUnify::Apart); - } - - s1.arguments - .iter() - .zip(s2.arguments.iter()) - .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { - result.and_then(|| can_unify(state, context, a1, a2)) - }) - } - _ => Ok(CanUnify::Apart), - } -} - -/// Validates that all rows in instance declaration arguments -/// do not have labels in non-determined positions. -/// -/// In PureScript, instance declarations can only contain rows with labels -/// in positions that are determined by functional dependencies. In the -/// determiner position, only row variables such as `( | r )` are valid. -pub fn validate_instance_rows( - state: &mut CheckState, - context: &CheckContext, - class_file: FileId, - class_item: TypeItemId, - arguments: &[TypeId], -) -> QueryResult<()> -where - Q: ExternalQueries, -{ - let functional_dependencies = get_functional_dependencies(context, class_file, class_item)?; - let all_determined = get_all_determined(&functional_dependencies); - - for (position, &argument_type) in arguments.iter().enumerate() { - if all_determined.contains(&position) { - continue; - } - - if HasLabeledRole::contains(state, context, argument_type)? { - let type_message = state.pretty_id(context, argument_type)?; - state.insert_error(ErrorKind::InstanceHeadLabeledRow { - class_file, - class_item, - position, - type_message, - }); - } - } - - Ok(()) -} - -struct CollectFileReferences<'a> { - files: &'a mut FxHashSet, -} - -impl<'a> CollectFileReferences<'a> { - fn collect( - state: &mut CheckState, - context: &CheckContext, - id: TypeId, - files: &'a mut FxHashSet, - ) -> QueryResult<()> - where - Q: ExternalQueries, - { - walk::walk_type(state, context, id, &mut CollectFileReferences { files }) - } -} - -impl TypeWalker for CollectFileReferences<'_> { - fn visit( - &mut self, - _state: &mut CheckState, - _context: &CheckContext, - _id: TypeId, - t: &Type, - ) -> QueryResult - where - Q: ExternalQueries, - { - if let Type::Constructor(file_id, _) = t { - self.files.insert(*file_id); - } - Ok(WalkAction::Continue) - } -} - -struct HasLabeledRole { - contains: bool, -} - -impl HasLabeledRole { - fn contains( - state: &mut CheckState, - context: &CheckContext, - id: TypeId, - ) -> QueryResult - where - Q: ExternalQueries, - { - let mut walker = HasLabeledRole { contains: false }; - walk::walk_type(state, context, id, &mut walker)?; - Ok(walker.contains) - } -} - -impl TypeWalker for HasLabeledRole { - fn visit( - &mut self, - _state: &mut CheckState, - context: &CheckContext, - _id: TypeId, - t: &Type, - ) -> QueryResult - where - Q: ExternalQueries, - { - if let Type::Row(row_id) = t { - let row = context.lookup_row_type(*row_id); - if !row.fields.is_empty() { - self.contains = true; - return Ok(WalkAction::Stop); - } - } - Ok(WalkAction::Continue) - } -} diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs new file mode 100644 index 00000000..ebf2691c --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -0,0 +1,217 @@ +use building_types::QueryResult; +use itertools::Itertools; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{Type, TypeId, normalise}; +use crate::state::CheckState; + +use super::{ + ConstraintApplication, MatchInstance, MatchType, constraint_application, elaborate_superclasses, +}; + +/// Discovers implied constraints from given constraints. +pub fn elaborate_given( + state: &mut CheckState, + context: &CheckContext, + given: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut elaborated = vec![]; + + for &constraint in given { + elaborated.push(constraint); + elaborate_superclasses(state, context, constraint, &mut elaborated)?; + } + + elaborated + .into_iter() + .map(|constraint| constraint_application(state, context, constraint)) + .filter_map_ok(|constraint| constraint) + .collect() +} + +/// Matches a wanted constraint to given constraints. +pub fn match_given_instances( + state: &mut CheckState, + context: &CheckContext, + wanted: &ConstraintApplication, + given: &[ConstraintApplication], +) -> QueryResult> +where + Q: ExternalQueries, +{ + 'given: for given in given { + if wanted.file_id != given.file_id || wanted.item_id != given.item_id { + continue; + } + + if wanted.arguments.len() != given.arguments.len() { + continue; + } + + let mut stuck_positions = vec![]; + + for (index, (&wanted_argument, &given_argument)) in + wanted.arguments.iter().zip(given.arguments.iter()).enumerate() + { + let match_result = match_given_type(state, context, wanted_argument, given_argument)?; + + if matches!(match_result, MatchType::Apart) { + continue 'given; + } + + if matches!(match_result, MatchType::Stuck) { + stuck_positions.push(index); + } + } + + // Given constraints are valid by construction. When a unification + // variable makes a position stuck, it's safe to emit an equality + // rather than require functional dependencies to cover it. + let equalities = stuck_positions + .into_iter() + .map(|index| (wanted.arguments[index], given.arguments[index])); + return Ok(Some(MatchInstance::Match { + constraints: vec![], + equalities: equalities.collect(), + })); + } + + Ok(None) +} + +/// Matches an argument from a wanted constraint to one from a given constraint. +/// +/// This function is specialised for matching given constraints, like those +/// found in value signatures rather than top-level instance declarations; +/// unlike [`match_type`], this function does not build bindings or equalities +/// for rigid variables. +/// +/// [`match_type`]: super::match_type +fn match_given_type( + state: &mut CheckState, + context: &CheckContext, + wanted: TypeId, + given: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let wanted = normalise::normalise(state, context, wanted)?; + let given = normalise::normalise(state, context, given)?; + + if wanted == given { + return Ok(MatchType::Match); + } + + let wanted_t = context.lookup_type(wanted); + let given_t = context.lookup_type(given); + + match (wanted_t, given_t) { + (Type::Unification(_), _) => Ok(MatchType::Stuck), + + (Type::Rigid(wname, _, wkind), Type::Rigid(gname, _, gkind)) => { + if wname == gname { + match_given_type(state, context, wkind, gkind) + } else { + Ok(MatchType::Apart) + } + } + + (Type::Application(wf, wa), Type::Application(gf, ga)) => { + match_given_type(state, context, wf, gf)? + .and_then(|| match_given_type(state, context, wa, ga)) + } + + (Type::Function(wa, wr), Type::Function(ga, gr)) => { + match_given_type(state, context, wa, ga)? + .and_then(|| match_given_type(state, context, wr, gr)) + } + + (Type::Function(wa, wr), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wa, wr); + match_given_type(state, context, wanted, given) + } + + (Type::Application(_, _), Type::Function(ga, gr)) => { + let given = context.intern_function_application(ga, gr); + match_given_type(state, context, wanted, given) + } + + (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { + let wanted_row = context.lookup_row_type(wanted_row_id); + let given_row = context.lookup_row_type(given_row_id); + + if wanted_row.fields.len() != given_row.fields.len() { + return Ok(MatchType::Apart); + } + + let mut result = MatchType::Match; + for (wanted_field, given_field) in wanted_row.fields.iter().zip(given_row.fields.iter()) + { + if wanted_field.label != given_field.label { + return Ok(MatchType::Apart); + } + result = result.and_then(|| { + match_given_type(state, context, wanted_field.id, given_field.id) + })?; + } + + match (wanted_row.tail, given_row.tail) { + (Some(wanted_tail), Some(given_tail)) => { + result.and_then(|| match_given_type(state, context, wanted_tail, given_tail)) + } + (Some(wanted_tail), None) => { + let wanted_tail = normalise::normalise(state, context, wanted_tail)?; + if matches!(context.lookup_type(wanted_tail), Type::Unification(_)) { + Ok(MatchType::Stuck) + } else { + Ok(MatchType::Apart) + } + } + (None, Some(given_tail)) => { + let given_tail = normalise::normalise(state, context, given_tail)?; + if matches!(context.lookup_type(given_tail), Type::Unification(_)) { + Ok(MatchType::Stuck) + } else { + Ok(MatchType::Apart) + } + } + (None, None) => Ok(result), + } + } + + (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { + match_given_type(state, context, wf, gf)? + .and_then(|| match_given_type(state, context, wa, ga)) + } + + (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) + if wf == gf && wi == gi => + { + match_given_type(state, context, wl, gl)? + .and_then(|| match_given_type(state, context, wr, gr)) + } + + (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { + let wsyn = context.lookup_synonym(wsyn); + let gsyn = context.lookup_synonym(gsyn); + + if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + return Ok(MatchType::Apart); + } + + wsyn.arguments + .iter() + .zip(gsyn.arguments.iter()) + .try_fold(MatchType::Match, |result, (&wa, &ga)| { + result.and_then(|| match_given_type(state, context, wa, ga)) + }) + } + + _ => Ok(MatchType::Apart), + } +} diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs new file mode 100644 index 00000000..2f6c64d7 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -0,0 +1,717 @@ +use std::collections::HashSet; + +use building_types::QueryResult; +use files::FileId; +use indexing::{InstanceChainId, TypeItemId}; +use itertools::Itertools; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::walk::{self, TypeWalker, WalkAction}; +use crate::core::{CheckedInstance, ForallBinder, Name, Type, TypeId, normalise, toolkit}; +use crate::error::ErrorKind; +use crate::state::CheckState; +use crate::{CheckedModule, ExternalQueries, safe_loop}; + +use super::functional_dependency::{Fd, compute_closure, get_all_determined}; +use super::{ConstraintApplication, MatchInstance, MatchType, constraint_application}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CanUnify { + Equal, + Apart, + Unify, +} + +impl CanUnify { + fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { + if let CanUnify::Equal = self { f() } else { Ok(self) } + } +} + +#[derive(Clone)] +pub struct CandidateInstance { + chain_id: Option, + position: u32, + checked: CheckedInstance, +} + +struct DecomposedInstance { + binders: Vec, + arguments: Vec, + constraints: Vec, +} + +/// Collects instance chains for a constraint from all eligible files. +pub fn collect_instance_chains( + state: &mut CheckState, + context: &CheckContext, + application: &ConstraintApplication, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + let mut files_to_search = FxHashSet::default(); + files_to_search.insert(application.file_id); + + for &argument in &application.arguments { + CollectFileReferences::collect(state, context, argument, &mut files_to_search)?; + } + + let mut instances = vec![]; + + for &file_id in &files_to_search { + if file_id == context.id { + collect_instances_from_checked( + &mut instances, + &state.checked, + &context.indexed, + application.file_id, + application.item_id, + ); + } else { + let checked = context.queries.checked2(file_id)?; + let indexed = context.queries.indexed(file_id)?; + collect_instances_from_checked( + &mut instances, + &checked, + &indexed, + application.file_id, + application.item_id, + ); + } + } + + let mut grouped: FxHashMap> = FxHashMap::default(); + let mut singleton = vec![]; + + for instance in instances { + if let Some(chain_id) = instance.chain_id { + grouped.entry(chain_id).or_default().push(instance); + } else { + singleton.push(vec![instance]); + } + } + + let mut chains = singleton; + for (_, mut chain) in grouped { + chain.sort_by_key(|instance| instance.position); + chains.push(chain); + } + + Ok(chains) +} + +fn collect_instances_from_checked( + output: &mut Vec, + checked: &CheckedModule, + indexed: &indexing::IndexedModule, + class_file: FileId, + class_id: TypeItemId, +) { + output.extend( + checked + .instances + .iter() + .filter(|(_, instance)| instance.resolution == (class_file, class_id)) + .map(|(&id, checked)| CandidateInstance { + chain_id: indexed.pairs.instance_chain_id(id), + position: indexed.pairs.instance_chain_position(id).unwrap_or(0), + checked: checked.clone(), + }), + ); +} + +fn get_functional_dependencies( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + fn extract(type_item: Option<&lowering::TypeItemIr>) -> Vec { + let Some(lowering::TypeItemIr::ClassGroup { class: Some(class), .. }) = type_item else { + return vec![]; + }; + + class + .functional_dependencies + .iter() + .map(|functional_dependency| { + Fd::new( + functional_dependency.determiners.iter().map(|&x| x as usize), + functional_dependency.determined.iter().map(|&x| x as usize), + ) + }) + .collect() + } + + if file_id == context.id { + Ok(extract(context.lowered.info.get_type_item(item_id))) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(extract(lowered.info.get_type_item(item_id))) + } +} + +/// Determines if [`MatchType::Stuck`] arguments can be determined by functional dependencies. +fn can_determine_stuck( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, + match_results: &[MatchType], + stuck_positions: &[usize], +) -> QueryResult +where + Q: ExternalQueries, +{ + if stuck_positions.is_empty() { + return Ok(true); + } + + let functional_dependencies = get_functional_dependencies(context, file_id, item_id)?; + let initial: HashSet<_> = match_results + .iter() + .enumerate() + .filter_map(|(index, result)| matches!(result, MatchType::Match).then_some(index)) + .collect(); + + let determined = compute_closure(&functional_dependencies, &initial); + Ok(stuck_positions.iter().all(|index| determined.contains(index))) +} + +fn decompose_instance( + state: &mut CheckState, + context: &CheckContext, + instance: &CheckedInstance, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { binders, quantified } = + toolkit::inspect_quantified(state, context, instance.canonical)?; + + let mut current = quantified; + let mut constraints = vec![]; + + safe_loop! { + current = normalise::normalise(state, context, current)?; + match context.lookup_type(current) { + Type::Constrained(constraint, constrained) => { + constraints.push(constraint); + current = constrained; + } + _ => break, + } + } + + let Some(application) = constraint_application(state, context, current)? else { + return Ok(None); + }; + + if (application.file_id, application.item_id) != instance.resolution { + return Ok(None); + } + + Ok(Some(DecomposedInstance { binders, arguments: application.arguments, constraints })) +} + +/// Matches a wanted constraint to an instance. +pub fn match_instance( + state: &mut CheckState, + context: &CheckContext, + wanted: &ConstraintApplication, + instance: &CandidateInstance, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(decomposed) = decompose_instance(state, context, &instance.checked)? else { + return Ok(MatchInstance::Apart); + }; + + if wanted.arguments.len() != decomposed.arguments.len() { + return Ok(MatchInstance::Apart); + } + + let mut bindings = FxHashMap::default(); + let mut equalities = vec![]; + let mut match_results = vec![]; + let mut stuck_positions = vec![]; + + for (index, (&wanted_argument, &instance_argument)) in + wanted.arguments.iter().zip(decomposed.arguments.iter()).enumerate() + { + let match_result = match_type( + state, + context, + &mut bindings, + &mut equalities, + wanted_argument, + instance_argument, + )?; + + if matches!(match_result, MatchType::Apart) { + return Ok(MatchInstance::Apart); + } + + if matches!(match_result, MatchType::Stuck) { + stuck_positions.push(index); + } + + match_results.push(match_result); + } + + if !stuck_positions.is_empty() + && !can_determine_stuck( + context, + wanted.file_id, + wanted.item_id, + &match_results, + &stuck_positions, + )? + { + return Ok(MatchInstance::Stuck); + } + + for &index in &stuck_positions { + let substituted = + SubstituteName::many(state, context, &bindings, decomposed.arguments[index])?; + equalities.push((wanted.arguments[index], substituted)); + } + + for binder in &decomposed.binders { + bindings + .entry(binder.name) + .or_insert_with(|| state.fresh_unification(context.queries, binder.kind)); + } + + let constraints = decomposed + .constraints + .into_iter() + .map(|constraint| SubstituteName::many(state, context, &bindings, constraint)) + .collect::>>()?; + + Ok(MatchInstance::Match { constraints, equalities }) +} + +/// Matches an argument from a wanted constraint to one from an instance. +/// +/// This function emits substitutions and equalities when matching against +/// instances, for example: +/// +/// ```purescript +/// instance TypeEq a a True +/// ``` +/// +/// When matching the wanted constraint `TypeEq Int ?0` against this instance, +/// it creates the binding `a := Int`. This means that subsequent usages of `a` +/// are equal to `Int`, turning the second `match(a, ?0)` into `match(Int, ?0)`. +/// We use the [`can_unify`] function to speculate if these two types can be +/// unified, or if unifying them solves unification variables, encoded by the +/// [`CanUnify::Unify`] variant. +fn match_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &mut FxHashMap, + equalities: &mut Vec<(TypeId, TypeId)>, + wanted: TypeId, + given: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let wanted = normalise::normalise(state, context, wanted)?; + let given = normalise::normalise(state, context, given)?; + + if wanted == given { + return Ok(MatchType::Match); + } + + let wanted_t = context.lookup_type(wanted); + let given_t = context.lookup_type(given); + + match (wanted_t, given_t) { + (_, Type::Rigid(name, _, _)) => { + if let Some(&bound) = bindings.get(&name) { + match can_unify(state, context, wanted, bound)? { + CanUnify::Equal => Ok(MatchType::Match), + CanUnify::Apart => Ok(MatchType::Apart), + CanUnify::Unify => { + equalities.push((wanted, bound)); + Ok(MatchType::Match) + } + } + } else { + bindings.insert(name, wanted); + Ok(MatchType::Match) + } + } + + (Type::Unification(_), _) => Ok(MatchType::Stuck), + + (Type::Row(wanted_row_id), Type::Row(given_row_id)) => { + let wanted_row = context.lookup_row_type(wanted_row_id); + let given_row = context.lookup_row_type(given_row_id); + match_row_type(state, context, bindings, equalities, wanted_row, given_row) + } + + (Type::Application(wf, wa), Type::Application(gf, ga)) => { + match_type(state, context, bindings, equalities, wf, gf)? + .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + } + + (Type::Function(wa, wr), Type::Function(ga, gr)) => { + match_type(state, context, bindings, equalities, wa, ga)? + .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) + } + + (Type::Function(wa, wr), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wa, wr); + match_type(state, context, bindings, equalities, wanted, given) + } + + (Type::Application(_, _), Type::Function(ga, gr)) => { + let given = context.intern_function_application(ga, gr); + match_type(state, context, bindings, equalities, wanted, given) + } + + (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { + match_type(state, context, bindings, equalities, wf, gf)? + .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + } + + (Type::Kinded(wi, wk), Type::Kinded(gi, gk)) => { + match_type(state, context, bindings, equalities, wi, gi)? + .and_then(|| match_type(state, context, bindings, equalities, wk, gk)) + } + + (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) + if wf == gf && wi == gi => + { + match_type(state, context, bindings, equalities, wl, gl)? + .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) + } + + (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { + let wsyn = context.lookup_synonym(wsyn); + let gsyn = context.lookup_synonym(gsyn); + + if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + return Ok(MatchType::Apart); + } + + wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( + MatchType::Match, + |result, (&wa, &ga)| { + result.and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + }, + ) + } + + _ => Ok(MatchType::Apart), + } +} + +/// Matches row types in instance heads. +/// +/// This function handles structural row matching for both the tail variable +/// form `( | r )` in determiner positions and labeled rows in determined +/// positions `( x :: T | r )`. This function partitions the two row types, +/// matches the shared fields, and handles the row tail. +fn match_row_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &mut FxHashMap, + equalities: &mut Vec<(TypeId, TypeId)>, + wanted_row: crate::core::RowType, + given_row: crate::core::RowType, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut wanted_only = vec![]; + let mut given_only = vec![]; + let mut result = MatchType::Match; + + for field in itertools::merge_join_by( + wanted_row.fields.iter(), + given_row.fields.iter(), + |wanted, given| wanted.label.cmp(&given.label), + ) { + match field { + itertools::EitherOrBoth::Both(wanted, given) => { + result = result.and_then(|| { + match_type(state, context, bindings, equalities, wanted.id, given.id) + })?; + // Given an open wanted row, additional fields from the + // given row can be absorbed into the wanted row's tail. + if matches!(result, MatchType::Apart) && wanted_row.tail.is_none() { + return Ok(MatchType::Apart); + } + } + itertools::EitherOrBoth::Left(wanted) => wanted_only.push(wanted), + itertools::EitherOrBoth::Right(given) => given_only.push(given), + } + } + + enum RowRest { + /// `( a :: Int )` and `( a :: Int | r )` + Additional, + /// `( | r )` + Open(TypeId), + /// `( )` + Closed, + } + + impl RowRest { + fn new(only: &[&crate::core::RowField], tail: Option) -> RowRest { + if !only.is_empty() { + RowRest::Additional + } else if let Some(tail) = tail { + RowRest::Open(tail) + } else { + RowRest::Closed + } + } + } + + use RowRest::*; + + let given_rest = RowRest::new(&given_only, given_row.tail); + let wanted_rest = RowRest::new(&wanted_only, wanted_row.tail); + + match given_rest { + // If there are additional given fields + Additional => match wanted_rest { + // we cannot match it against a tail-less wanted, + // nor against the additional wanted fields. + Closed | Additional => Ok(MatchType::Apart), + // we could potentially make progress by having the + // wanted tail absorb the additional given fields + Open(_) => Ok(MatchType::Stuck), + }, + // If the given row has a tail, match it against the + // additional fields and tail from the wanted row + Open(given_tail) => { + let fields = wanted_only.into_iter().cloned().collect_vec(); + let row = context.intern_row( + context.intern_row_type(crate::core::RowType::new(fields, wanted_row.tail)), + ); + result.and_then(|| match_type(state, context, bindings, equalities, row, given_tail)) + } + // If we have a closed given row + Closed => match wanted_rest { + // we cannot match it against fields in the wanted row + Additional => Ok(MatchType::Apart), + // we could make progress with an open wanted row + Open(_) => Ok(MatchType::Stuck), + // we can match it directly with a closed wanted row + Closed => Ok(result), + }, + } +} + +/// Determines if two types [`CanUnify`]. +/// +/// This is used in [`match_type`], where if two different types bind to the +/// same rigid variable, we determine if the types can actually unify before +/// generating an equality. This is effectively a pure version of the +/// [`unification::unify`] function. +fn can_unify( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(CanUnify::Equal); + } + + let t1_core = context.lookup_type(t1); + let t2_core = context.lookup_type(t2); + + match (t1_core, t2_core) { + (Type::Unification(_), _) | (_, Type::Unification(_)) => Ok(CanUnify::Unify), + (Type::Unknown(_), _) | (_, Type::Unknown(_)) => Ok(CanUnify::Unify), + (Type::Row(_), Type::Row(_)) => Ok(CanUnify::Unify), + + (Type::Application(f1, a1), Type::Application(f2, a2)) => { + can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) + } + (Type::Function(a1, r1), Type::Function(a2, r2)) => { + can_unify(state, context, a1, a2)?.and_then(|| can_unify(state, context, r1, r2)) + } + // Function(a, b) and Application(Application(f, a), b) can + // unify when `f` resolves to the Function constructor. + (Type::Function(..), Type::Application(..)) + | (Type::Application(..), Type::Function(..)) => Ok(CanUnify::Unify), + (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) => { + can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) + } + (Type::Kinded(i1, k1), Type::Kinded(i2, k2)) => { + can_unify(state, context, i1, i2)?.and_then(|| can_unify(state, context, k1, k2)) + } + (Type::Constrained(c1, i1), Type::Constrained(c2, i2)) => { + can_unify(state, context, c1, c2)?.and_then(|| can_unify(state, context, i1, i2)) + } + (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { + if n1 == n2 { + can_unify(state, context, k1, k2) + } else { + Ok(CanUnify::Apart) + } + } + (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { + let b1 = context.lookup_forall_binder(b1); + let b2 = context.lookup_forall_binder(b2); + can_unify(state, context, b1.kind, b2.kind)? + .and_then(|| can_unify(state, context, i1, i2)) + } + (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { + if f1 == f2 && i1 == i2 { + can_unify(state, context, l1, l2)?.and_then(|| can_unify(state, context, r1, r2)) + } else { + Ok(CanUnify::Apart) + } + } + (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { + let s1 = context.lookup_synonym(s1); + let s2 = context.lookup_synonym(s2); + if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { + return Ok(CanUnify::Apart); + } + + s1.arguments + .iter() + .zip(s2.arguments.iter()) + .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { + result.and_then(|| can_unify(state, context, a1, a2)) + }) + } + _ => Ok(CanUnify::Apart), + } +} + +/// Validates that all rows in instance declaration arguments +/// do not have labels in non-determined positions. +/// +/// In PureScript, instance declarations can only contain rows with labels +/// in positions that are determined by functional dependencies. In the +/// determiner position, only row variables such as `( | r )` are valid. +pub fn validate_rows( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_item: TypeItemId, + arguments: &[TypeId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let functional_dependencies = get_functional_dependencies(context, class_file, class_item)?; + let all_determined = get_all_determined(&functional_dependencies); + + for (position, &argument_type) in arguments.iter().enumerate() { + if all_determined.contains(&position) { + continue; + } + + if HasLabeledRole::contains(state, context, argument_type)? { + let type_message = state.pretty_id(context, argument_type)?; + state.insert_error(ErrorKind::InstanceHeadLabeledRow { + class_file, + class_item, + position, + type_message, + }); + } + } + + Ok(()) +} + +struct CollectFileReferences<'a> { + files: &'a mut FxHashSet, +} + +impl<'a> CollectFileReferences<'a> { + fn collect( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + files: &'a mut FxHashSet, + ) -> QueryResult<()> + where + Q: ExternalQueries, + { + walk::walk_type(state, context, id, &mut CollectFileReferences { files }) + } +} + +impl TypeWalker for CollectFileReferences<'_> { + fn visit( + &mut self, + _state: &mut CheckState, + _context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Constructor(file_id, _) = t { + self.files.insert(*file_id); + } + Ok(WalkAction::Continue) + } +} + +struct HasLabeledRole { + contains: bool, +} + +impl HasLabeledRole { + fn contains( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + ) -> QueryResult + where + Q: ExternalQueries, + { + let mut walker = HasLabeledRole { contains: false }; + walk::walk_type(state, context, id, &mut walker)?; + Ok(walker.contains) + } +} + +impl TypeWalker for HasLabeledRole { + fn visit( + &mut self, + _state: &mut CheckState, + context: &CheckContext, + _id: TypeId, + t: &Type, + ) -> QueryResult + where + Q: ExternalQueries, + { + if let Type::Row(row_id) = t { + let row = context.lookup_row_type(*row_id); + if !row.fields.is_empty() { + self.contains = true; + return Ok(WalkAction::Stop); + } + } + Ok(WalkAction::Continue) + } +} diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 5eb9466e..733545df 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -135,7 +135,7 @@ where canonical = context.intern_constrained(constraint, canonical); } - constraint::validate_instance_rows(state, context, class_file, class_id, &checked_arguments)?; + constraint::instances::validate_rows(state, context, class_file, class_id, &checked_arguments)?; let resolution = (class_file, class_id); let canonical = zonk::zonk(state, context, canonical)?; From ff3d07fd913de8b2feb4db70164198b1ee7be3b1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 28 Feb 2026 17:54:45 +0800 Subject: [PATCH 285/386] Add integration tests for instance solving --- .../044_instance_constraint_solving/Main.purs | 19 +++++++++++++++ .../044_instance_constraint_solving/Main.snap | 21 ++++++++++++++++ .../Main.purs | 15 ++++++++++++ .../Main.snap | 24 +++++++++++++++++++ .../046_instance_given_constraint/Main.purs | 14 +++++++++++ .../046_instance_given_constraint/Main.snap | 19 +++++++++++++++ .../Main.purs | 12 ++++++++++ .../Main.snap | 20 ++++++++++++++++ .../Main.purs | 10 ++++++++ .../Main.snap | 19 +++++++++++++++ .../tests/checking2/generated.rs | 10 ++++++++ 11 files changed, 183 insertions(+) create mode 100644 tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.purs create mode 100644 tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.snap create mode 100644 tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.purs create mode 100644 tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.snap create mode 100644 tests-integration/fixtures/checking2/046_instance_given_constraint/Main.purs create mode 100644 tests-integration/fixtures/checking2/046_instance_given_constraint/Main.snap create mode 100644 tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.purs create mode 100644 tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.snap create mode 100644 tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.purs create mode 100644 tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.snap diff --git a/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.purs b/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.purs new file mode 100644 index 00000000..7a12ea78 --- /dev/null +++ b/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.purs @@ -0,0 +1,19 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a where + eq :: a -> a -> Boolean + +instance Eq Int where + eq _ _ = true + +instance Eq a => Eq (Array a) where + eq _ _ = false + +test :: Boolean +test = eq 1 2 + +test2 :: Boolean +test2 = eq [1] [2] + +test3 x y = eq x y diff --git a/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.snap b/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.snap new file mode 100644 index 00000000..a9b23664 --- /dev/null +++ b/tests-integration/fixtures/checking2/044_instance_constraint_solving/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +test :: Boolean +test2 :: Boolean +test3 :: forall (t2 :: Type). Eq (t2 :: Type) => (t2 :: Type) -> (t2 :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Instances +instance Eq Int +instance forall (t1 :: Type). Eq (t1 :: Type) => Eq (Array (t1 :: Type)) diff --git a/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.purs b/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.purs new file mode 100644 index 00000000..1208a6ba --- /dev/null +++ b/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.purs @@ -0,0 +1,15 @@ +module Main where + +class Convert :: Type -> Type -> Constraint +class Convert a b | a -> b where + convert :: a -> b + +instance Convert Int String where + convert _ = "int" + +test = convert 42 + +class TypeEq :: forall k. k -> k -> Constraint +class TypeEq a b | a -> b, b -> a + +instance TypeEq a a diff --git a/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.snap b/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.snap new file mode 100644 index 00000000..a80d1fd8 --- /dev/null +++ b/tests-integration/fixtures/checking2/045_instance_functional_dependency/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +test :: String + +Types +Convert :: Type -> Type -> Constraint +TypeEq :: forall (k :: Type). (k :: Type) -> (k :: Type) -> Constraint + +Classes +class forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) + convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +class forall (k :: Type) (a :: (k :: Type)) (b :: (k :: Type)). TypeEq @(k :: Type) (a :: (k :: Type)) (b :: (k :: Type)) + +Instances +instance Convert Int String +instance forall (t6 :: Type) (t5 :: (t6 :: Type)). + TypeEq @(t6 :: Type) (t5 :: (t6 :: Type)) (t5 :: (t6 :: Type)) diff --git a/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.purs b/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.purs new file mode 100644 index 00000000..6393608f --- /dev/null +++ b/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.purs @@ -0,0 +1,14 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a where + eq :: a -> a -> Boolean + +instance Eq Int where + eq _ _ = true + +instance Eq a => Eq (Array a) where + eq _ _ = false + +test :: Boolean +test = eq [[1]] [[2]] diff --git a/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.snap b/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.snap new file mode 100644 index 00000000..fda8a49c --- /dev/null +++ b/tests-integration/fixtures/checking2/046_instance_given_constraint/Main.snap @@ -0,0 +1,19 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +test :: Boolean + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Instances +instance Eq Int +instance forall (t1 :: Type). Eq (t1 :: Type) => Eq (Array (t1 :: Type)) diff --git a/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.purs b/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.purs new file mode 100644 index 00000000..3d5f07fd --- /dev/null +++ b/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.purs @@ -0,0 +1,12 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a where + eq :: a -> a -> Boolean + +class Ord :: Type -> Constraint +class Ord a where + compare :: a -> a -> Int + +test1 x = if eq x x then eq x x else false +test2 x = if eq x x then compare x x else compare x x diff --git a/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.snap b/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.snap new file mode 100644 index 00000000..ebf481f3 --- /dev/null +++ b/tests-integration/fixtures/checking2/047_instance_constraint_generalization/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int +test1 :: forall (t2 :: Type). Eq (t2 :: Type) => (t2 :: Type) -> Boolean +test2 :: forall (t3 :: Type). Ord (t3 :: Type) => Eq (t3 :: Type) => (t3 :: Type) -> Int + +Types +Eq :: Type -> Constraint +Ord :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +class forall (a :: Type). Ord (a :: Type) + compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int diff --git a/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.purs b/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.purs new file mode 100644 index 00000000..72ceded3 --- /dev/null +++ b/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.purs @@ -0,0 +1,10 @@ +module Main where + +class Eq :: Type -> Constraint +class Eq a where + eq :: a -> a -> Boolean + +class Eq a <= Ord a where + compare :: a -> a -> Int + +test x = if eq x x then compare x x else compare x x diff --git a/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.snap b/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.snap new file mode 100644 index 00000000..32e67be2 --- /dev/null +++ b/tests-integration/fixtures/checking2/048_instance_superclass_elaboration/Main.snap @@ -0,0 +1,19 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int +test :: forall (t2 :: Type). Ord (t2 :: Type) => (t2 :: Type) -> Int + +Types +Eq :: Type -> Constraint +Ord :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +class forall (a :: Type). Eq (a :: Type) <= Ord (a :: Type) + compare :: forall (a :: Type). Ord (a :: Type) => (a :: Type) -> (a :: Type) -> Int diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index dc066bee..09d232af 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -115,3 +115,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_042_data_mutual_infer_main() { run_test("042_data_mutual_infer", "Main"); } #[rustfmt::skip] #[test] fn test_043_instance_check_main() { run_test("043_instance_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_044_instance_constraint_solving_main() { run_test("044_instance_constraint_solving", "Main"); } + +#[rustfmt::skip] #[test] fn test_045_instance_functional_dependency_main() { run_test("045_instance_functional_dependency", "Main"); } + +#[rustfmt::skip] #[test] fn test_046_instance_given_constraint_main() { run_test("046_instance_given_constraint", "Main"); } + +#[rustfmt::skip] #[test] fn test_047_instance_constraint_generalization_main() { run_test("047_instance_constraint_generalization", "Main"); } + +#[rustfmt::skip] #[test] fn test_048_instance_superclass_elaboration_main() { run_test("048_instance_superclass_elaboration", "Main"); } From a2952007daafd3498a67cc811bb85189aee18ab8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 02:41:07 +0800 Subject: [PATCH 286/386] Fix missing collect_givens in signature checking --- compiler-core/checking2/src/core/toolkit.rs | 20 +++++++++++++++++++ compiler-core/checking2/src/source.rs | 1 + .../checking2/src/source/term_items.rs | 2 ++ .../checking2/src/source/terms/form_let.rs | 2 ++ .../049_given_constraint_check/Main.purs | 7 +++++++ .../049_given_constraint_check/Main.snap | 15 ++++++++++++++ .../050_given_constraint_let_check/Main.purs | 10 ++++++++++ .../050_given_constraint_let_check/Main.snap | 15 ++++++++++++++ .../Main.purs | 6 ++++++ .../Main.snap | 15 ++++++++++++++ .../tests/checking2/generated.rs | 6 ++++++ 11 files changed, 99 insertions(+) create mode 100644 tests-integration/fixtures/checking2/049_given_constraint_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/049_given_constraint_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.snap create mode 100644 tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.purs create mode 100644 tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.snap diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 3c2f68a3..d627d4f9 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -357,6 +357,26 @@ where } } +/// Peels constraint layers without introducing givens or wanteds. +pub fn without_constraints( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Constrained(_, constrained) => { + id = constrained; + } + _ => return Ok(id), + } + } +} + /// Instantiates forall binders and collects wanted constraints. pub fn instantiate_constrained( state: &mut CheckState, diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 6f431841..deecf04a 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -31,6 +31,7 @@ where { let toolkit::InspectQuantified { quantified, .. } = toolkit::inspect_quantified(state, context, kind)?; + let quantified = toolkit::without_constraints(state, context, quantified)?; let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, quantified)?; diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 733545df..6e4f6740 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -309,6 +309,8 @@ where let toolkit::InspectQuantified { quantified, .. } = toolkit::inspect_quantified(state, context, signature_type)?; + let quantified = toolkit::collect_givens(state, context, quantified)?; + let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function(state, context, quantified)?; diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 74edec30..5bafdaa0 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -142,6 +142,8 @@ where let toolkit::InspectQuantified { quantified, .. } = toolkit::inspect_quantified(state, context, name_type)?; + let quantified = toolkit::collect_givens(state, context, quantified)?; + let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function(state, context, quantified)?; diff --git a/tests-integration/fixtures/checking2/049_given_constraint_check/Main.purs b/tests-integration/fixtures/checking2/049_given_constraint_check/Main.purs new file mode 100644 index 00000000..4e240c92 --- /dev/null +++ b/tests-integration/fixtures/checking2/049_given_constraint_check/Main.purs @@ -0,0 +1,7 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean + +test :: forall a. Eq a => a -> Boolean +test a = eq a a diff --git a/tests-integration/fixtures/checking2/049_given_constraint_check/Main.snap b/tests-integration/fixtures/checking2/049_given_constraint_check/Main.snap new file mode 100644 index 00000000..8fafd85d --- /dev/null +++ b/tests-integration/fixtures/checking2/049_given_constraint_check/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +test :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.purs b/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.purs new file mode 100644 index 00000000..7b48edbe --- /dev/null +++ b/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.purs @@ -0,0 +1,10 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean + +test :: Int +test = 42 + where + impl :: forall a. Eq a => a -> Boolean + impl a = eq a a diff --git a/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.snap b/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.snap new file mode 100644 index 00000000..128a5aaf --- /dev/null +++ b/tests-integration/fixtures/checking2/050_given_constraint_let_check/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +test :: Int + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.purs b/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.purs new file mode 100644 index 00000000..829fcaf5 --- /dev/null +++ b/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.purs @@ -0,0 +1,6 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean + +infix 5 eq as == diff --git a/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.snap b/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.snap new file mode 100644 index 00000000..3d9a4cdd --- /dev/null +++ b/tests-integration/fixtures/checking2/051_given_constraint_operator_check/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +== :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 09d232af..0b20c9c1 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -125,3 +125,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_047_instance_constraint_generalization_main() { run_test("047_instance_constraint_generalization", "Main"); } #[rustfmt::skip] #[test] fn test_048_instance_superclass_elaboration_main() { run_test("048_instance_superclass_elaboration", "Main"); } + +#[rustfmt::skip] #[test] fn test_049_given_constraint_check_main() { run_test("049_given_constraint_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_050_given_constraint_let_check_main() { run_test("050_given_constraint_let_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_051_given_constraint_operator_check_main() { run_test("051_given_constraint_operator_check", "Main"); } From 73814760527d5a969ec87434d0c29b64a0dd2c97 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 01:48:10 +0800 Subject: [PATCH 287/386] Add scaffolding for compiler-solved instances --- .../checking2/src/core/constraint.rs | 19 +++++ .../checking2/src/core/constraint/compiler.rs | 75 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 compiler-core/checking2/src/core/constraint/compiler.rs diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index 01d95eb3..4594b373 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -1,3 +1,4 @@ +pub mod compiler; pub mod functional_dependency; pub mod given; pub mod instances; @@ -17,6 +18,7 @@ use crate::core::{Type, TypeId, normalise, toolkit, unification}; use crate::implication::ImplicationId; use crate::state::CheckState; +use compiler::match_compiler_instances; use given::{elaborate_given, match_given_instances}; use instances::{collect_instance_chains, match_instance}; @@ -129,6 +131,23 @@ where Some(MatchInstance::Apart | MatchInstance::Stuck) | None => {} } + match match_compiler_instances(state, context, &application, &given)? { + Some(MatchInstance::Match { constraints, equalities }) => { + for (t1, t2) in equalities { + if unification::unify(state, context, t1, t2)? { + made_progress = true; + } + } + work_queue.extend(constraints); + continue 'work; + } + Some(MatchInstance::Stuck) => { + residual.push(wanted); + continue 'work; + } + Some(MatchInstance::Apart) | None => {} + } + let instance_chains = collect_instance_chains(state, context, &application)?; for chain in instance_chains { diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs new file mode 100644 index 00000000..e66ad398 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -0,0 +1,75 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::state::CheckState; + +use super::{ConstraintApplication, MatchInstance}; + +pub fn match_compiler_instances( + _state: &mut CheckState, + context: &CheckContext, + wanted: &ConstraintApplication, + _given: &[ConstraintApplication], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let ConstraintApplication { file_id, item_id, arguments: _ } = *wanted; + + let match_instance = if file_id == context.prim_int.file_id { + if item_id == context.prim_int.add { + None + } else if item_id == context.prim_int.mul { + None + } else if item_id == context.prim_int.compare { + None + } else if item_id == context.prim_int.to_string { + None + } else { + None + } + } else if file_id == context.prim_symbol.file_id { + if item_id == context.prim_symbol.append { + None + } else if item_id == context.prim_symbol.compare { + None + } else if item_id == context.prim_symbol.cons { + None + } else { + None + } + } else if file_id == context.prim_row.file_id { + if item_id == context.prim_row.union { + None + } else if item_id == context.prim_row.cons { + None + } else if item_id == context.prim_row.lacks { + None + } else if item_id == context.prim_row.nub { + None + } else { + None + } + } else if file_id == context.prim_row_list.file_id { + if item_id == context.prim_row_list.row_to_list { None } else { None } + } else if file_id == context.prim_coerce.file_id { + if item_id == context.prim_coerce.coercible { None } else { None } + } else if file_id == context.prim_type_error.file_id { + if item_id == context.prim_type_error.warn { + None + } else if item_id == context.prim_type_error.fail { + None + } else { + None + } + } else if context.known_reflectable.is_symbol == Some((file_id, item_id)) { + None + } else if context.known_reflectable.reflectable == Some((file_id, item_id)) { + None + } else { + None + }; + + Ok(match_instance) +} From 4cad9b66383de767938e3e7b8bb790cfe19e2a66 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 01:51:10 +0800 Subject: [PATCH 288/386] Move can_unify from instances to unification --- .../src/core/constraint/instances.rs | 101 +------------- .../checking2/src/core/unification.rs | 124 ++++++++++++++++++ 2 files changed, 125 insertions(+), 100 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index 2f6c64d7..e4c07331 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -8,6 +8,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; +use crate::core::unification::{CanUnify, can_unify}; use crate::core::walk::{self, TypeWalker, WalkAction}; use crate::core::{CheckedInstance, ForallBinder, Name, Type, TypeId, normalise, toolkit}; use crate::error::ErrorKind; @@ -17,19 +18,6 @@ use crate::{CheckedModule, ExternalQueries, safe_loop}; use super::functional_dependency::{Fd, compute_closure, get_all_determined}; use super::{ConstraintApplication, MatchInstance, MatchType, constraint_application}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum CanUnify { - Equal, - Apart, - Unify, -} - -impl CanUnify { - fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { - if let CanUnify::Equal = self { f() } else { Ok(self) } - } -} - #[derive(Clone)] pub struct CandidateInstance { chain_id: Option, @@ -514,93 +502,6 @@ where } } -/// Determines if two types [`CanUnify`]. -/// -/// This is used in [`match_type`], where if two different types bind to the -/// same rigid variable, we determine if the types can actually unify before -/// generating an equality. This is effectively a pure version of the -/// [`unification::unify`] function. -fn can_unify( - state: &mut CheckState, - context: &CheckContext, - t1: TypeId, - t2: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; - - if t1 == t2 { - return Ok(CanUnify::Equal); - } - - let t1_core = context.lookup_type(t1); - let t2_core = context.lookup_type(t2); - - match (t1_core, t2_core) { - (Type::Unification(_), _) | (_, Type::Unification(_)) => Ok(CanUnify::Unify), - (Type::Unknown(_), _) | (_, Type::Unknown(_)) => Ok(CanUnify::Unify), - (Type::Row(_), Type::Row(_)) => Ok(CanUnify::Unify), - - (Type::Application(f1, a1), Type::Application(f2, a2)) => { - can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) - } - (Type::Function(a1, r1), Type::Function(a2, r2)) => { - can_unify(state, context, a1, a2)?.and_then(|| can_unify(state, context, r1, r2)) - } - // Function(a, b) and Application(Application(f, a), b) can - // unify when `f` resolves to the Function constructor. - (Type::Function(..), Type::Application(..)) - | (Type::Application(..), Type::Function(..)) => Ok(CanUnify::Unify), - (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) => { - can_unify(state, context, f1, f2)?.and_then(|| can_unify(state, context, a1, a2)) - } - (Type::Kinded(i1, k1), Type::Kinded(i2, k2)) => { - can_unify(state, context, i1, i2)?.and_then(|| can_unify(state, context, k1, k2)) - } - (Type::Constrained(c1, i1), Type::Constrained(c2, i2)) => { - can_unify(state, context, c1, c2)?.and_then(|| can_unify(state, context, i1, i2)) - } - (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { - if n1 == n2 { - can_unify(state, context, k1, k2) - } else { - Ok(CanUnify::Apart) - } - } - (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { - let b1 = context.lookup_forall_binder(b1); - let b2 = context.lookup_forall_binder(b2); - can_unify(state, context, b1.kind, b2.kind)? - .and_then(|| can_unify(state, context, i1, i2)) - } - (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { - if f1 == f2 && i1 == i2 { - can_unify(state, context, l1, l2)?.and_then(|| can_unify(state, context, r1, r2)) - } else { - Ok(CanUnify::Apart) - } - } - (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { - let s1 = context.lookup_synonym(s1); - let s2 = context.lookup_synonym(s2); - if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { - return Ok(CanUnify::Apart); - } - - s1.arguments - .iter() - .zip(s2.arguments.iter()) - .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { - result.and_then(|| can_unify(state, context, a1, a2)) - }) - } - _ => Ok(CanUnify::Apart), - } -} - /// Validates that all rows in instance declaration arguments /// do not have labels in non-determined positions. /// diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 03e20be3..4967406d 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -1,5 +1,6 @@ //! Implements the subtyping and unification algorithms. +use std::iter; use std::sync::Arc; use building_types::QueryResult; @@ -14,6 +15,19 @@ use crate::error::ErrorKind; use crate::source::types; use crate::state::{CheckState, UnificationEntry}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CanUnify { + Equal, + Apart, + Unify, +} + +impl CanUnify { + pub fn and_then(self, f: impl FnOnce() -> QueryResult) -> QueryResult { + if let CanUnify::Equal = self { f() } else { Ok(self) } + } +} + /// Strategy for handling constrained types during [`subtype_with`]. /// /// Elaboration involves pushing constraints as "wanted". This behaviour is @@ -369,6 +383,116 @@ where Ok(unifies) } +/// Determines if two types [`CanUnify`]. +/// +/// This is a pure, speculative version of [`unify`] that does not perform +/// any side effects. It is used in instance matching to check whether two +/// types bound to the same rigid variable can actually unify before +/// generating an equality. +pub fn can_unify( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(CanUnify::Equal); + } + + let t1_core = context.lookup_type(t1); + let t2_core = context.lookup_type(t2); + + match (t1_core, t2_core) { + (Type::Unification(_), _) | (_, Type::Unification(_)) => Ok(CanUnify::Unify), + (Type::Unknown(_), _) | (_, Type::Unknown(_)) => Ok(CanUnify::Unify), + (Type::Row(_), Type::Row(_)) => Ok(CanUnify::Unify), + + ( + Type::Application(t1_function, t1_argument), + Type::Application(t2_function, a22t2_argument), + ) => can_unify(state, context, t1_function, t2_function)? + .and_then(|| can_unify(state, context, t1_argument, a22t2_argument)), + + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { + can_unify(state, context, t1_argument, t2_argument)? + .and_then(|| can_unify(state, context, t1_result, t2_result)) + } + + // Function(a, b) and Application(Application(f, a), b) can + // unify when `f` resolves to the Function constructor. + (Type::Function(..), Type::Application(..)) + | (Type::Application(..), Type::Function(..)) => Ok(CanUnify::Unify), + + ( + Type::KindApplication(t1_function, t1_argument), + Type::KindApplication(t2_function, t2_argument), + ) => can_unify(state, context, t1_function, t2_function)? + .and_then(|| can_unify(state, context, t1_argument, t2_argument)), + + (Type::Kinded(t1_inner, t1_kind), Type::Kinded(t2_inner, t2_kind)) => { + can_unify(state, context, t1_inner, t2_inner)? + .and_then(|| can_unify(state, context, t1_kind, t2_kind)) + } + + ( + Type::Constrained(t1_constraint, t1_constrained), + Type::Constrained(t2_constraint, t2_constrained), + ) => can_unify(state, context, t1_constraint, t2_constraint)? + .and_then(|| can_unify(state, context, t1_constrained, t2_constrained)), + + (Type::Rigid(t1_name, _, t1_kind), Type::Rigid(t2_name, _, t2_kind)) => { + if t1_name == t2_name { + can_unify(state, context, t1_kind, t2_kind) + } else { + Ok(CanUnify::Apart) + } + } + + (Type::Forall(t1_binder, t1_inner), Type::Forall(t2_binder, t2_inner)) => { + let t1_binder = context.lookup_forall_binder(t1_binder); + let t2_binder = context.lookup_forall_binder(t2_binder); + can_unify(state, context, t1_binder.kind, t2_binder.kind)? + .and_then(|| can_unify(state, context, t1_inner, t2_inner)) + } + + ( + Type::OperatorApplication(t1_file, t1_item, t1_left, t1_right), + Type::OperatorApplication(t2_file, t2_item, t2_left, t2_right), + ) => { + if t1_file == t2_file && t1_item == t2_item { + can_unify(state, context, t1_left, t2_left)? + .and_then(|| can_unify(state, context, t1_right, t2_right)) + } else { + Ok(CanUnify::Apart) + } + } + + (Type::SynonymApplication(t1_synonym), Type::SynonymApplication(t2_synonym)) => { + let t1_synonym = context.lookup_synonym(t1_synonym); + let t2_synonym = context.lookup_synonym(t2_synonym); + + if t1_synonym.reference != t2_synonym.reference + || t1_synonym.arguments.len() != t2_synonym.arguments.len() + { + return Ok(CanUnify::Apart); + } + + iter::zip(&*t1_synonym.arguments, &*t2_synonym.arguments) + .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { + result.and_then(|| can_unify(state, context, a1, a2)) + }) + } + + _ => Ok(CanUnify::Apart), + } +} + /// Solves a unification variable to a given type. pub fn solve( state: &mut CheckState, From b54b7ab409d48b2912f2a1f7abadd920232375cb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 01:51:10 +0800 Subject: [PATCH 289/386] Implement initial compiler-solved helpers --- .../checking2/src/core/constraint/compiler.rs | 64 +++++++++++++++++++ compiler-core/checking2/src/core/toolkit.rs | 41 +++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index e66ad398..8c67e7f7 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -1,11 +1,75 @@ use building_types::QueryResult; +use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; +use crate::core::{RowType, Type, TypeId, normalise}; use crate::state::CheckState; use super::{ConstraintApplication, MatchInstance}; +pub fn extract_integer( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Integer(value) => Ok(Some(value)), + _ => Ok(None), + } +} + +pub fn extract_symbol( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + if let Type::String(_, id) = context.lookup_type(id) { + Ok(Some(context.queries.lookup_smol_str(id))) + } else { + Ok(None) + } +} + +pub fn extract_row( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + if let Type::Row(id) = context.lookup_type(id) { + Ok(Some(context.lookup_row_type(id))) + } else { + Ok(None) + } +} + +pub fn intern_symbol(context: &CheckContext, value: &str) -> TypeId +where + Q: ExternalQueries, +{ + let smol_str_id = context.queries.intern_smol_str(SmolStr::new(value)); + context.queries.intern_type(Type::String(lowering::StringKind::String, smol_str_id)) +} + +pub fn intern_integer(context: &CheckContext, value: i32) -> TypeId +where + Q: ExternalQueries, +{ + context.queries.intern_type(Type::Integer(value)) +} + pub fn match_compiler_instances( _state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index d627d4f9..ce46c968 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -1,5 +1,7 @@ //! Implements shared utilities for core type operations. +use std::sync::Arc; + use building_types::QueryResult; use files::FileId; use indexing::{TermItemId, TypeItemId}; @@ -7,7 +9,7 @@ use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, Type, TypeId, normalise, unification, + CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, normalise, unification, }; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -437,3 +439,40 @@ where _ => Ok(None), } } + +pub fn lookup_file_roles( + state: &mut CheckState, + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult>> +where + Q: ExternalQueries, +{ + if file_id == context.id { + Ok(state.checked.lookup_roles(item_id)) + } else { + let checked = context.queries.checked2(file_id)?; + Ok(checked.lookup_roles(item_id)) + } +} + +pub fn is_newtype( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let type_item = if file_id == context.id { + context.lowered.info.get_type_item(item_id) + } else { + let lowered = context.queries.lowered(file_id)?; + return Ok(matches!( + lowered.info.get_type_item(item_id), + Some(lowering::TypeItemIr::NewtypeGroup { .. }) + )); + }; + Ok(matches!(type_item, Some(lowering::TypeItemIr::NewtypeGroup { .. }))) +} From d456f524cd153e2e19e675b4c2e162bfb83c7ea1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 01:57:58 +0800 Subject: [PATCH 290/386] Implement Prim.Int, Prim.Symbol, Prim.RowList --- .../checking2/src/core/constraint/compiler.rs | 94 +++++---- .../src/core/constraint/compiler/prim_int.rs | 187 ++++++++++++++++++ .../core/constraint/compiler/prim_row_list.rs | 75 +++++++ .../core/constraint/compiler/prim_symbol.rs | 171 ++++++++++++++++ .../fixtures/checking2/052_prim_int/Main.purs | 44 +++++ .../fixtures/checking2/052_prim_int/Main.snap | 33 ++++ .../053_prim_int_compare_transitive/Main.purs | 26 +++ .../053_prim_int_compare_transitive/Main.snap | 37 ++++ .../checking2/054_prim_symbol/Main.purs | 47 +++++ .../checking2/054_prim_symbol/Main.snap | 43 ++++ .../checking2/055_prim_solver_apart/Main.purs | 25 +++ .../checking2/055_prim_solver_apart/Main.snap | 44 +++++ .../checking2/056_prim_row_list/Main.purs | 27 +++ .../checking2/056_prim_row_list/Main.snap | 40 ++++ .../tests/checking2/generated.rs | 10 + 15 files changed, 861 insertions(+), 42 deletions(-) create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_int.rs create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs create mode 100644 tests-integration/fixtures/checking2/052_prim_int/Main.purs create mode 100644 tests-integration/fixtures/checking2/052_prim_int/Main.snap create mode 100644 tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.purs create mode 100644 tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.snap create mode 100644 tests-integration/fixtures/checking2/054_prim_symbol/Main.purs create mode 100644 tests-integration/fixtures/checking2/054_prim_symbol/Main.snap create mode 100644 tests-integration/fixtures/checking2/055_prim_solver_apart/Main.purs create mode 100644 tests-integration/fixtures/checking2/055_prim_solver_apart/Main.snap create mode 100644 tests-integration/fixtures/checking2/056_prim_row_list/Main.purs create mode 100644 tests-integration/fixtures/checking2/056_prim_row_list/Main.snap diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index 8c67e7f7..65946135 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -1,8 +1,13 @@ +mod prim_int; +mod prim_row_list; +mod prim_symbol; + use building_types::QueryResult; use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; +use crate::core::unification::{CanUnify, can_unify}; use crate::core::{RowType, Type, TypeId, normalise}; use crate::state::CheckState; @@ -70,66 +75,71 @@ where context.queries.intern_type(Type::Integer(value)) } +fn match_equality( + state: &mut CheckState, + context: &CheckContext, + actual: TypeId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + Ok(match can_unify(state, context, actual, expected)? { + CanUnify::Apart => MatchInstance::Apart, + CanUnify::Equal | CanUnify::Unify => { + MatchInstance::Match { constraints: vec![], equalities: vec![(actual, expected)] } + } + }) +} + pub fn match_compiler_instances( - _state: &mut CheckState, + state: &mut CheckState, context: &CheckContext, wanted: &ConstraintApplication, - _given: &[ConstraintApplication], + given: &[ConstraintApplication], ) -> QueryResult> where Q: ExternalQueries, { - let ConstraintApplication { file_id, item_id, arguments: _ } = *wanted; + let ConstraintApplication { file_id, item_id, arguments } = wanted; - let match_instance = if file_id == context.prim_int.file_id { - if item_id == context.prim_int.add { - None - } else if item_id == context.prim_int.mul { - None - } else if item_id == context.prim_int.compare { - None - } else if item_id == context.prim_int.to_string { - None + let match_instance = if *file_id == context.prim_int.file_id { + if *item_id == context.prim_int.add { + prim_int::match_add(state, context, arguments)? + } else if *item_id == context.prim_int.mul { + prim_int::match_mul(state, context, arguments)? + } else if *item_id == context.prim_int.compare { + prim_int::match_compare(state, context, arguments, given)? + } else if *item_id == context.prim_int.to_string { + prim_int::match_to_string(state, context, arguments)? } else { None } - } else if file_id == context.prim_symbol.file_id { - if item_id == context.prim_symbol.append { - None - } else if item_id == context.prim_symbol.compare { - None - } else if item_id == context.prim_symbol.cons { - None - } else { - None - } - } else if file_id == context.prim_row.file_id { - if item_id == context.prim_row.union { - None - } else if item_id == context.prim_row.cons { - None - } else if item_id == context.prim_row.lacks { - None - } else if item_id == context.prim_row.nub { - None + } else if *file_id == context.prim_symbol.file_id { + if *item_id == context.prim_symbol.append { + prim_symbol::match_append(state, context, arguments)? + } else if *item_id == context.prim_symbol.compare { + prim_symbol::match_compare(state, context, arguments)? + } else if *item_id == context.prim_symbol.cons { + prim_symbol::match_cons(state, context, arguments)? } else { None } - } else if file_id == context.prim_row_list.file_id { - if item_id == context.prim_row_list.row_to_list { None } else { None } - } else if file_id == context.prim_coerce.file_id { - if item_id == context.prim_coerce.coercible { None } else { None } - } else if file_id == context.prim_type_error.file_id { - if item_id == context.prim_type_error.warn { - None - } else if item_id == context.prim_type_error.fail { - None + } else if *file_id == context.prim_row.file_id { + None + } else if *file_id == context.prim_row_list.file_id { + if *item_id == context.prim_row_list.row_to_list { + prim_row_list::match_row_to_list(state, context, arguments)? } else { None } - } else if context.known_reflectable.is_symbol == Some((file_id, item_id)) { + } else if *file_id == context.prim_coerce.file_id { + None + } else if *file_id == context.prim_type_error.file_id { None - } else if context.known_reflectable.reflectable == Some((file_id, item_id)) { + } else if context.known_reflectable.is_symbol == Some((*file_id, *item_id)) { + prim_symbol::match_is_symbol(state, context, arguments)? + } else if context.known_reflectable.reflectable == Some((*file_id, *item_id)) { None } else { None diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs new file mode 100644 index 00000000..8d623f8b --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs @@ -0,0 +1,187 @@ +use std::cmp::Ordering; + +use building_types::QueryResult; +use petgraph::algo::has_path_connecting; +use petgraph::prelude::DiGraphMap; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::{ConstraintApplication, MatchInstance}; +use crate::core::{TypeId, normalise}; +use crate::state::CheckState; + +use super::{extract_integer, intern_symbol, match_equality}; + +pub fn match_add( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, sum] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let sum = normalise::normalise(state, context, sum)?; + + let left_int = extract_integer(state, context, left)?; + let right_int = extract_integer(state, context, right)?; + let sum_int = extract_integer(state, context, sum)?; + + let matched = match (left_int, right_int, sum_int) { + (Some(left), Some(right), _) => { + let result = super::intern_integer(context, left + right); + match_equality(state, context, sum, result)? + } + (Some(left), _, Some(sum_value)) => { + let result = super::intern_integer(context, sum_value - left); + match_equality(state, context, right, result)? + } + (_, Some(right), Some(sum_value)) => { + let result = super::intern_integer(context, sum_value - right); + match_equality(state, context, left, result)? + } + _ => MatchInstance::Stuck, + }; + + Ok(Some(matched)) +} + +pub fn match_mul( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, product] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let product = normalise::normalise(state, context, product)?; + + let Some(left_int) = extract_integer(state, context, left)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + let Some(right_int) = extract_integer(state, context, right)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let result = super::intern_integer(context, left_int * right_int); + Ok(Some(match_equality(state, context, product, result)?)) +} + +pub fn match_compare( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], + given: &[ConstraintApplication], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, ordering] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let ordering = normalise::normalise(state, context, ordering)?; + + let left_int = extract_integer(state, context, left)?; + let right_int = extract_integer(state, context, right)?; + + if let (Some(left_int), Some(right_int)) = (left_int, right_int) { + let result = match left_int.cmp(&right_int) { + Ordering::Less => context.prim_ordering.lt, + Ordering::Equal => context.prim_ordering.eq, + Ordering::Greater => context.prim_ordering.gt, + }; + + return Ok(Some(match_equality(state, context, ordering, result)?)); + } + + Ok(Some(match_compare_transitive(state, context, left, right, ordering, given)?)) +} + +// Compare givens induce a directed graph where an edge `a -> b` means `a < b`. +fn match_compare_transitive( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, + ordering: TypeId, + given: &[ConstraintApplication], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut graph: DiGraphMap = DiGraphMap::new(); + + for constraint in given { + if constraint.file_id != context.prim_int.file_id + || constraint.item_id != context.prim_int.compare + { + continue; + } + + let &[a, b, relation] = constraint.arguments.as_slice() else { + continue; + }; + + let a = normalise::normalise(state, context, a)?; + let b = normalise::normalise(state, context, b)?; + let relation = normalise::normalise(state, context, relation)?; + + if relation == context.prim_ordering.lt { + graph.add_edge(a, b, ()); + } else if relation == context.prim_ordering.eq { + graph.add_edge(a, b, ()); + graph.add_edge(b, a, ()); + } else if relation == context.prim_ordering.gt { + graph.add_edge(b, a, ()); + } + } + + let left_reaches_right = has_path_connecting(&graph, left, right, None); + let right_reaches_left = has_path_connecting(&graph, right, left, None); + + let result = match (left_reaches_right, right_reaches_left) { + (true, true) => context.prim_ordering.eq, + (true, false) => context.prim_ordering.lt, + (false, true) => context.prim_ordering.gt, + (false, false) => return Ok(MatchInstance::Stuck), + }; + + match_equality(state, context, ordering, result) +} + +pub fn match_to_string( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[int, symbol] = arguments else { + return Ok(None); + }; + + let int = normalise::normalise(state, context, int)?; + let symbol = normalise::normalise(state, context, symbol)?; + + let Some(value) = extract_integer(state, context, int)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let result = intern_symbol(context, &value.to_string()); + Ok(Some(match_equality(state, context, symbol, result)?)) +} diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs new file mode 100644 index 00000000..9dec2fa6 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::{RowField, RowType, TypeId, normalise}; +use crate::source::types; +use crate::state::CheckState; + +use super::{extract_row, intern_symbol, match_equality}; + +pub fn match_row_to_list( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[row, list] = arguments else { + return Ok(None); + }; + + let row = normalise::normalise(state, context, row)?; + let list = normalise::normalise(state, context, list)?; + + let Some(row_value) = extract_row(state, context, row)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + if row_value.tail.is_some() { + return Ok(Some(MatchInstance::Stuck)); + } + + let element_kind = row_element_kind(state, context, &row_value)?; + + let nil = context.intern_kind_application(context.prim_row_list.nil, element_kind); + let cons = context.intern_kind_application(context.prim_row_list.cons, element_kind); + + let result = row_value.fields.iter().rev().fold(nil, |rest, field| { + let label = intern_symbol(context, field.label.as_str()); + let cons_label = context.intern_application(cons, label); + let cons_type = context.intern_application(cons_label, field.id); + context.intern_application(cons_type, rest) + }); + + Ok(Some(match_equality(state, context, list, result)?)) +} + +fn row_element_kind( + state: &mut CheckState, + context: &CheckContext, + row: &RowType, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(RowField { id, .. }) = row.fields.first() else { + return Ok(state.fresh_unification(context.queries, context.prim.t)); + }; + + let fields = Arc::from([RowField { label: row.fields[0].label.clone(), id: *id }]); + let singleton_row = RowType { fields, tail: None }; + let singleton_row_id = context.intern_row_type(singleton_row); + let singleton_row_type = context.intern_row(singleton_row_id); + let row_kind = types::elaborate_kind(state, context, singleton_row_type)?; + + let row_kind = normalise::normalise(state, context, row_kind)?; + let crate::core::Type::Application(_, element_kind) = context.lookup_type(row_kind) else { + return Ok(state.fresh_unification(context.queries, context.prim.t)); + }; + + Ok(element_kind) +} diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs new file mode 100644 index 00000000..f17848bf --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs @@ -0,0 +1,171 @@ +use std::cmp::Ordering; + +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::{Type, TypeId, normalise}; +use crate::state::CheckState; + +use super::{extract_symbol, intern_symbol, match_equality}; + +pub fn match_append( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, appended] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let appended = normalise::normalise(state, context, appended)?; + + let left_symbol = extract_symbol(state, context, left)?; + let right_symbol = extract_symbol(state, context, right)?; + let appended_symbol = extract_symbol(state, context, appended)?; + + let matched = match (left_symbol, right_symbol, appended_symbol) { + (Some(left_value), Some(right_value), _) => { + let result = intern_symbol(context, &format!("{left_value}{right_value}")); + match_equality(state, context, appended, result)? + } + (_, Some(right_value), Some(appended_value)) => { + let Some(left_value) = appended_value.strip_suffix(right_value.as_str()) else { + return Ok(Some(MatchInstance::Apart)); + }; + + let result = intern_symbol(context, left_value); + match_equality(state, context, left, result)? + } + (Some(left_value), _, Some(appended_value)) => { + let Some(right_value) = appended_value.strip_prefix(left_value.as_str()) else { + return Ok(Some(MatchInstance::Apart)); + }; + + let result = intern_symbol(context, right_value); + match_equality(state, context, right, result)? + } + _ => MatchInstance::Stuck, + }; + + Ok(Some(matched)) +} + +pub fn match_compare( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, ordering] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let ordering = normalise::normalise(state, context, ordering)?; + + let Some(left_symbol) = extract_symbol(state, context, left)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + let Some(right_symbol) = extract_symbol(state, context, right)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let result = match left_symbol.cmp(&right_symbol) { + Ordering::Less => context.prim_ordering.lt, + Ordering::Equal => context.prim_ordering.eq, + Ordering::Greater => context.prim_ordering.gt, + }; + + Ok(Some(match_equality(state, context, ordering, result)?)) +} + +pub fn match_cons( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[head, tail, symbol] = arguments else { + return Ok(None); + }; + + let head = normalise::normalise(state, context, head)?; + let tail = normalise::normalise(state, context, tail)?; + let symbol = normalise::normalise(state, context, symbol)?; + + let head_symbol = extract_symbol(state, context, head)?; + let tail_symbol = extract_symbol(state, context, tail)?; + let symbol_symbol = extract_symbol(state, context, symbol)?; + + let matched = match (&head_symbol, &tail_symbol, &symbol_symbol) { + (Some(head_value), Some(tail_value), _) => { + let mut chars = head_value.chars(); + let (Some(ch), None) = (chars.next(), chars.next()) else { + return Ok(Some(MatchInstance::Apart)); + }; + + let result = intern_symbol(context, &format!("{ch}{tail_value}")); + match_equality(state, context, symbol, result)? + } + (_, _, Some(symbol_value)) => { + let mut chars = symbol_value.chars(); + let Some(head_char) = chars.next() else { + return Ok(Some(MatchInstance::Apart)); + }; + + if let Some(head_value) = head_symbol { + let mut head_chars = head_value.chars(); + if head_chars.next() != Some(head_char) || head_chars.next().is_some() { + return Ok(Some(MatchInstance::Apart)); + } + } + + let head_result = intern_symbol(context, &head_char.to_string()); + let tail_result = intern_symbol(context, chars.as_str()); + MatchInstance::Match { + constraints: vec![], + equalities: vec![(head, head_result), (tail, tail_result)], + } + } + _ => MatchInstance::Stuck, + }; + + Ok(Some(matched)) +} + +pub fn match_is_symbol( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[symbol] = arguments else { + return Ok(None); + }; + + let symbol = normalise::normalise(state, context, symbol)?; + + let matched = if extract_symbol(state, context, symbol)?.is_some() { + MatchInstance::Match { constraints: vec![], equalities: vec![] } + } else if matches!(context.lookup_type(symbol), Type::Unification(_)) { + MatchInstance::Stuck + } else { + MatchInstance::Apart + }; + + Ok(Some(matched)) +} diff --git a/tests-integration/fixtures/checking2/052_prim_int/Main.purs b/tests-integration/fixtures/checking2/052_prim_int/Main.purs new file mode 100644 index 00000000..22f910c9 --- /dev/null +++ b/tests-integration/fixtures/checking2/052_prim_int/Main.purs @@ -0,0 +1,44 @@ +module Main where + +import Prim.Int (class Add, class Compare, class Mul, class ToString) +import Prim.Ordering (EQ, GT, LT) +import Type.Proxy (Proxy(..)) + +deriveSum :: forall sum. Add 1 2 sum => Proxy sum +deriveSum = Proxy + +deriveRight :: forall right. Add 1 right 3 => Proxy right +deriveRight = Proxy + +deriveLeft :: forall left. Add left 2 3 => Proxy left +deriveLeft = Proxy + +stuckAdd :: forall left sum. Add left 1 sum => Proxy left -> Proxy sum +stuckAdd _ = Proxy + +deriveMul :: forall product. Mul 3 4 product => Proxy product +deriveMul = Proxy + +compareLT :: forall ord. Compare 1 2 ord => Proxy ord +compareLT = Proxy + +compareEQ :: forall ord. Compare 5 5 ord => Proxy ord +compareEQ = Proxy + +compareGT :: forall ord. Compare 10 3 ord => Proxy ord +compareGT = Proxy + +deriveString :: forall s. ToString 42 s => Proxy s +deriveString = Proxy + +forceSolve = + { deriveSum + , deriveRight + , deriveLeft + , deriveMul + , compareLT + , compareEQ + , compareGT + , deriveString + , keepStuck: stuckAdd (Proxy :: Proxy 0) + } diff --git a/tests-integration/fixtures/checking2/052_prim_int/Main.snap b/tests-integration/fixtures/checking2/052_prim_int/Main.snap new file mode 100644 index 00000000..88ecaf19 --- /dev/null +++ b/tests-integration/fixtures/checking2/052_prim_int/Main.snap @@ -0,0 +1,33 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +deriveSum :: forall (sum :: Int). Add 1 2 (sum :: Int) => Proxy @Int (sum :: Int) +deriveRight :: forall (right :: Int). Add 1 (right :: Int) 3 => Proxy @Int (right :: Int) +deriveLeft :: forall (left :: Int). Add (left :: Int) 2 3 => Proxy @Int (left :: Int) +stuckAdd :: + forall (left :: Int) (sum :: Int). + Add (left :: Int) 1 (sum :: Int) => Proxy @Int (left :: Int) -> Proxy @Int (sum :: Int) +deriveMul :: forall (product :: Int). Mul 3 4 (product :: Int) => Proxy @Int (product :: Int) +compareLT :: + forall (ord :: Ordering). Compare 1 2 (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +compareEQ :: + forall (ord :: Ordering). Compare 5 5 (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +compareGT :: + forall (ord :: Ordering). Compare 10 3 (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +deriveString :: forall (s :: Symbol). ToString 42 (s :: Symbol) => Proxy @Symbol (s :: Symbol) +forceSolve :: + { compareEQ :: Proxy @Ordering EQ + , compareGT :: Proxy @Ordering GT + , compareLT :: Proxy @Ordering LT + , deriveLeft :: Proxy @Int 1 + , deriveMul :: Proxy @Int 12 + , deriveRight :: Proxy @Int 2 + , deriveString :: Proxy @Symbol "42" + , deriveSum :: Proxy @Int 3 + , keepStuck :: Proxy @Int 1 + } + +Types diff --git a/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.purs b/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.purs new file mode 100644 index 00000000..4ef2952a --- /dev/null +++ b/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.purs @@ -0,0 +1,26 @@ +module Main where + +import Prim.Int (class Compare) +import Prim.Ordering (EQ, GT, LT) +import Type.Proxy (Proxy(..)) + +assertLesser :: forall l r. Compare l r LT => Proxy ( left :: l, right :: r ) +assertLesser = Proxy + +assertGreater :: forall l r. Compare l r GT => Proxy ( left :: l, right :: r ) +assertGreater = Proxy + +assertEqual :: forall l r. Compare l r EQ => Proxy ( left :: l, right :: r ) +assertEqual = Proxy + +transLt :: forall m n p. Compare m n LT => Compare n p LT => Proxy n -> Proxy ( left :: m, right :: p ) +transLt _ = assertLesser + +transLtEq :: forall m n p. Compare m n LT => Compare n p EQ => Proxy n -> Proxy ( left :: m, right :: p ) +transLtEq _ = assertLesser + +transEqGt :: forall m n p. Compare m n EQ => Compare n p GT => Proxy n -> Proxy ( left :: m, right :: p ) +transEqGt _ = assertGreater + +transSymmEq :: forall m n p. Compare n m EQ => Compare n p EQ => Proxy n -> Proxy ( left :: m, right :: p ) +transSymmEq _ = assertEqual diff --git a/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.snap b/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.snap new file mode 100644 index 00000000..32ad64d9 --- /dev/null +++ b/tests-integration/fixtures/checking2/053_prim_int_compare_transitive/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +assertLesser :: + forall (l :: Int) (r :: Int). + Compare (l :: Int) (r :: Int) LT => Proxy @(Row Int) ( left :: (l :: Int), right :: (r :: Int) ) +assertGreater :: + forall (l :: Int) (r :: Int). + Compare (l :: Int) (r :: Int) GT => Proxy @(Row Int) ( left :: (l :: Int), right :: (r :: Int) ) +assertEqual :: + forall (l :: Int) (r :: Int). + Compare (l :: Int) (r :: Int) EQ => Proxy @(Row Int) ( left :: (l :: Int), right :: (r :: Int) ) +transLt :: + forall (m :: Int) (n :: Int) (p :: Int). + Compare (m :: Int) (n :: Int) LT => + Compare (n :: Int) (p :: Int) LT => + Proxy @Int (n :: Int) -> Proxy @(Row Int) ( left :: (m :: Int), right :: (p :: Int) ) +transLtEq :: + forall (m :: Int) (n :: Int) (p :: Int). + Compare (m :: Int) (n :: Int) LT => + Compare (n :: Int) (p :: Int) EQ => + Proxy @Int (n :: Int) -> Proxy @(Row Int) ( left :: (m :: Int), right :: (p :: Int) ) +transEqGt :: + forall (m :: Int) (n :: Int) (p :: Int). + Compare (m :: Int) (n :: Int) EQ => + Compare (n :: Int) (p :: Int) GT => + Proxy @Int (n :: Int) -> Proxy @(Row Int) ( left :: (m :: Int), right :: (p :: Int) ) +transSymmEq :: + forall (m :: Int) (n :: Int) (p :: Int). + Compare (n :: Int) (m :: Int) EQ => + Compare (n :: Int) (p :: Int) EQ => + Proxy @Int (n :: Int) -> Proxy @(Row Int) ( left :: (m :: Int), right :: (p :: Int) ) + +Types diff --git a/tests-integration/fixtures/checking2/054_prim_symbol/Main.purs b/tests-integration/fixtures/checking2/054_prim_symbol/Main.purs new file mode 100644 index 00000000..ac723674 --- /dev/null +++ b/tests-integration/fixtures/checking2/054_prim_symbol/Main.purs @@ -0,0 +1,47 @@ +module Main where + +import Data.Symbol (class IsSymbol, reflectSymbol) +import Prim.Symbol (class Append, class Compare, class Cons) +import Type.Proxy (Proxy(..)) + +deriveAppended :: forall appended. Append "Hello" "World" appended => Proxy appended +deriveAppended = Proxy + +deriveLeft :: forall left. Append left "World" "HelloWorld" => Proxy left +deriveLeft = Proxy + +deriveRight :: forall right. Append "Hello" right "HelloWorld" => Proxy right +deriveRight = Proxy + +compareLT :: forall ord. Compare "a" "b" ord => Proxy ord +compareLT = Proxy + +compareEQ :: forall ord. Compare "hello" "hello" ord => Proxy ord +compareEQ = Proxy + +compareGT :: forall ord. Compare "z" "a" ord => Proxy ord +compareGT = Proxy + +deriveCons :: forall symbol. Cons "H" "ello" symbol => Proxy symbol +deriveCons = Proxy + +deriveHeadTail :: forall head tail. Cons head tail "World" => Proxy head +deriveHeadTail = Proxy + +symbolValue :: String +symbolValue = reflectSymbol (Proxy :: Proxy "hello") + +symbolValue' = reflectSymbol (Proxy :: Proxy "") + +forceSolve = + { deriveAppended + , deriveLeft + , deriveRight + , compareLT + , compareEQ + , compareGT + , deriveCons + , deriveHeadTail + , symbolValue + , symbolValue' + } diff --git a/tests-integration/fixtures/checking2/054_prim_symbol/Main.snap b/tests-integration/fixtures/checking2/054_prim_symbol/Main.snap new file mode 100644 index 00000000..856f329c --- /dev/null +++ b/tests-integration/fixtures/checking2/054_prim_symbol/Main.snap @@ -0,0 +1,43 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +deriveAppended :: + forall (appended :: Symbol). + Append "Hello" "World" (appended :: Symbol) => Proxy @Symbol (appended :: Symbol) +deriveLeft :: + forall (left :: Symbol). + Append (left :: Symbol) "World" "HelloWorld" => Proxy @Symbol (left :: Symbol) +deriveRight :: + forall (right :: Symbol). + Append "Hello" (right :: Symbol) "HelloWorld" => Proxy @Symbol (right :: Symbol) +compareLT :: + forall (ord :: Ordering). Compare "a" "b" (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +compareEQ :: + forall (ord :: Ordering). + Compare "hello" "hello" (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +compareGT :: + forall (ord :: Ordering). Compare "z" "a" (ord :: Ordering) => Proxy @Ordering (ord :: Ordering) +deriveCons :: + forall (symbol :: Symbol). Cons "H" "ello" (symbol :: Symbol) => Proxy @Symbol (symbol :: Symbol) +deriveHeadTail :: + forall (head :: Symbol) (tail :: Symbol). + Cons (head :: Symbol) (tail :: Symbol) "World" => Proxy @Symbol (head :: Symbol) +symbolValue :: String +symbolValue' :: String +forceSolve :: + { compareEQ :: Proxy @Ordering EQ + , compareGT :: Proxy @Ordering GT + , compareLT :: Proxy @Ordering LT + , deriveAppended :: Proxy @Symbol "HelloWorld" + , deriveCons :: Proxy @Symbol "Hello" + , deriveHeadTail :: Proxy @Symbol "W" + , deriveLeft :: Proxy @Symbol "Hello" + , deriveRight :: Proxy @Symbol "World" + , symbolValue :: String + , symbolValue' :: String + } + +Types diff --git a/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.purs b/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.purs new file mode 100644 index 00000000..68f8fb1d --- /dev/null +++ b/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.purs @@ -0,0 +1,25 @@ +module Main where + +import Prim.Int as Int +import Prim.Ordering (LT) +import Prim.Symbol as Symbol +import Type.Proxy (Proxy(..)) + +invalidAdd :: Int.Add 2 3 10 => Proxy 10 +invalidAdd = Proxy + +invalidCompare :: Int.Compare 5 1 LT => Proxy LT +invalidCompare = Proxy + +invalidAppend :: Symbol.Append "hello" "world" "xyz" => Proxy "xyz" +invalidAppend = Proxy + +invalidCons :: Symbol.Cons "hello" "world" "helloworld" => Proxy "world" +invalidCons = Proxy + +forceSolve = + { invalidAdd + , invalidCompare + , invalidAppend + , invalidCons + } diff --git a/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.snap b/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.snap new file mode 100644 index 00000000..d51fcc9f --- /dev/null +++ b/tests-integration/fixtures/checking2/055_prim_solver_apart/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +invalidAdd :: Add 2 3 10 => Proxy @Int 10 +invalidCompare :: Compare 5 1 LT => Proxy @Ordering LT +invalidAppend :: Append "hello" "world" "xyz" => Proxy @Symbol "xyz" +invalidCons :: Cons "hello" "world" "helloworld" => Proxy @Symbol "world" +forceSolve :: + { invalidAdd :: Proxy @Int 10 + , invalidAppend :: Proxy @Symbol "xyz" + , invalidCompare :: Proxy @Ordering LT + , invalidCons :: Proxy @Symbol "world" + } + +Types + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(21), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(22), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(23), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(24), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/056_prim_row_list/Main.purs b/tests-integration/fixtures/checking2/056_prim_row_list/Main.purs new file mode 100644 index 00000000..215d6ed4 --- /dev/null +++ b/tests-integration/fixtures/checking2/056_prim_row_list/Main.purs @@ -0,0 +1,27 @@ +module Main where + +import Prim.RowList as RL +import Type.Proxy (Proxy(..)) + +rowToListSimple :: forall list. RL.RowToList (a :: Int) list => Proxy list +rowToListSimple = Proxy + +rowToListMultiple :: forall list. RL.RowToList (b :: String, a :: Int) list => Proxy list +rowToListMultiple = Proxy + +rowToListEmpty :: forall list. RL.RowToList () list => Proxy list +rowToListEmpty = Proxy + +rowToListThree :: forall list. RL.RowToList (c :: Boolean, a :: Int, b :: String) list => Proxy list +rowToListThree = Proxy + +stuckOpenRow :: forall tail list. RL.RowToList (a :: Int | tail) list => Proxy tail -> Proxy list +stuckOpenRow _ = Proxy + +forceSolve = + { rowToListSimple + , rowToListMultiple + , rowToListEmpty + , rowToListThree + , keepStuck: stuckOpenRow (Proxy :: Proxy ()) + } diff --git a/tests-integration/fixtures/checking2/056_prim_row_list/Main.snap b/tests-integration/fixtures/checking2/056_prim_row_list/Main.snap new file mode 100644 index 00000000..553a0257 --- /dev/null +++ b/tests-integration/fixtures/checking2/056_prim_row_list/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +rowToListSimple :: + forall (list :: RowList Type). + RowToList @Type ( a :: Int ) (list :: RowList Type) => + Proxy @(RowList Type) (list :: RowList Type) +rowToListMultiple :: + forall (list :: RowList Type). + RowToList @Type ( a :: Int, b :: String ) (list :: RowList Type) => + Proxy @(RowList Type) (list :: RowList Type) +rowToListEmpty :: + forall (t3 :: Type) (list :: RowList (t3 :: Type)). + RowToList @(t3 :: Type) () (list :: RowList (t3 :: Type)) => + Proxy @(RowList (t3 :: Type)) (list :: RowList (t3 :: Type)) +rowToListThree :: + forall (list :: RowList Type). + RowToList @Type ( a :: Int, b :: String, c :: Boolean ) (list :: RowList Type) => + Proxy @(RowList Type) (list :: RowList Type) +stuckOpenRow :: + forall (tail :: Row Type) (list :: RowList Type). + RowToList @Type ( a :: Int | (tail :: Row Type) ) (list :: RowList Type) => + Proxy @(Row Type) (tail :: Row Type) -> Proxy @(RowList Type) (list :: RowList Type) +forceSolve :: + forall (t8 :: RowList Type) (t7 :: Type). + RowToList @Type ( a :: Int ) (t8 :: RowList Type) => + { keepStuck :: Proxy @(RowList Type) (t8 :: RowList Type) + , rowToListEmpty :: Proxy @(RowList (t7 :: Type)) (Nil @(t7 :: Type)) + , rowToListMultiple :: + Proxy @(RowList Type) (Cons @Type "a" Int (Cons @Type "b" String (Nil @Type))) + , rowToListSimple :: Proxy @(RowList Type) (Cons @Type "a" Int (Nil @Type)) + , rowToListThree :: + Proxy @(RowList Type) + (Cons @Type "a" Int (Cons @Type "b" String (Cons @Type "c" Boolean (Nil @Type)))) + } + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 0b20c9c1..85f1a24a 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -131,3 +131,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_050_given_constraint_let_check_main() { run_test("050_given_constraint_let_check", "Main"); } #[rustfmt::skip] #[test] fn test_051_given_constraint_operator_check_main() { run_test("051_given_constraint_operator_check", "Main"); } + +#[rustfmt::skip] #[test] fn test_052_prim_int_main() { run_test("052_prim_int", "Main"); } + +#[rustfmt::skip] #[test] fn test_053_prim_int_compare_transitive_main() { run_test("053_prim_int_compare_transitive", "Main"); } + +#[rustfmt::skip] #[test] fn test_054_prim_symbol_main() { run_test("054_prim_symbol", "Main"); } + +#[rustfmt::skip] #[test] fn test_055_prim_solver_apart_main() { run_test("055_prim_solver_apart", "Main"); } + +#[rustfmt::skip] #[test] fn test_056_prim_row_list_main() { run_test("056_prim_row_list", "Main"); } From d8774af2d280e426465909c499bba3bd48dd1ed4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 04:18:59 +0800 Subject: [PATCH 291/386] Rename functional_dependency to fd --- compiler-core/checking2/src/core/constraint.rs | 2 +- .../src/core/constraint/{functional_dependency.rs => fd.rs} | 0 compiler-core/checking2/src/core/constraint/instances.rs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename compiler-core/checking2/src/core/constraint/{functional_dependency.rs => fd.rs} (100%) diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index 4594b373..c56ccb35 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -1,5 +1,5 @@ pub mod compiler; -pub mod functional_dependency; +pub mod fd; pub mod given; pub mod instances; diff --git a/compiler-core/checking2/src/core/constraint/functional_dependency.rs b/compiler-core/checking2/src/core/constraint/fd.rs similarity index 100% rename from compiler-core/checking2/src/core/constraint/functional_dependency.rs rename to compiler-core/checking2/src/core/constraint/fd.rs diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index e4c07331..dc46c25f 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -15,7 +15,7 @@ use crate::error::ErrorKind; use crate::state::CheckState; use crate::{CheckedModule, ExternalQueries, safe_loop}; -use super::functional_dependency::{Fd, compute_closure, get_all_determined}; +use super::fd::{Fd, compute_closure, get_all_determined}; use super::{ConstraintApplication, MatchInstance, MatchType, constraint_application}; #[derive(Clone)] From 2bf4150c816d06bf47390fa3cc0a892cc3a797fb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 22:22:36 +0800 Subject: [PATCH 292/386] Implement Prim.Row instances --- .../checking2/src/core/constraint/compiler.rs | 13 +- .../src/core/constraint/compiler/prim_row.rs | 304 ++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_row.rs diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index 65946135..c39b394f 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -1,4 +1,5 @@ mod prim_int; +mod prim_row; mod prim_row_list; mod prim_symbol; @@ -126,7 +127,17 @@ where None } } else if *file_id == context.prim_row.file_id { - None + if *item_id == context.prim_row.union { + prim_row::match_union(state, context, arguments)? + } else if *item_id == context.prim_row.cons { + prim_row::match_cons(state, context, arguments)? + } else if *item_id == context.prim_row.lacks { + prim_row::match_lacks(state, context, arguments)? + } else if *item_id == context.prim_row.nub { + prim_row::match_nub(state, context, arguments)? + } else { + None + } } else if *file_id == context.prim_row_list.file_id { if *item_id == context.prim_row_list.row_to_list { prim_row_list::match_row_to_list(state, context, arguments)? diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs new file mode 100644 index 00000000..c1624c20 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs @@ -0,0 +1,304 @@ +use std::cmp::Ordering; + +use building_types::QueryResult; +use itertools::Itertools; +use rustc_hash::FxHashSet; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::unification::{CanUnify, can_unify}; +use crate::core::{RowField, RowType, Type, TypeId, normalise}; +use crate::state::CheckState; + +use super::{extract_row, extract_symbol, match_equality}; + +fn intern_row_value(context: &CheckContext, row: RowType) -> TypeId +where + Q: ExternalQueries, +{ + let row_id = context.intern_row_type(row); + context.intern_row(row_id) +} + +fn extract_closed_row( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(row) = extract_row(state, context, id)? else { return Ok(None) }; + if row.tail.is_some() { + return Ok(None); + } + Ok(Some(row)) +} + +type SubtractResult = (Vec, Vec<(TypeId, TypeId)>); + +fn subtract_row_fields( + state: &mut CheckState, + context: &CheckContext, + source: &[RowField], + to_remove: &[RowField], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut result = vec![]; + let mut equalities = vec![]; + let mut to_remove_iter = to_remove.iter().peekable(); + + for field in source { + if let Some(remove_field) = to_remove_iter.peek() { + match field.label.cmp(&remove_field.label) { + Ordering::Less => { + result.push(field.clone()); + } + Ordering::Equal => { + if let CanUnify::Apart = can_unify(state, context, field.id, remove_field.id)? { + return Ok(None); + } + equalities.push((field.id, remove_field.id)); + to_remove_iter.next(); + } + Ordering::Greater => { + return Ok(None); + } + } + } else { + result.push(field.clone()); + } + } + + if to_remove_iter.next().is_some() { + return Ok(None); + } + + Ok(Some((result, equalities))) +} + +pub fn match_union( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right, union] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + let union = normalise::normalise(state, context, union)?; + + let left_row = extract_row(state, context, left)?; + let right_row = extract_row(state, context, right)?; + let union_row = extract_row(state, context, union)?; + + match (left_row, right_row, union_row) { + (Some(left_row), Some(right_row), _) => { + if let Some(rest) = left_row.tail { + if left_row.fields.is_empty() { + return Ok(Some(MatchInstance::Stuck)); + } + + let fresh_tail = state.fresh_unification(context.queries, context.prim.row_type); + + let result = intern_row_value( + context, + RowType::new(left_row.fields.iter().cloned(), Some(fresh_tail)), + ); + + let prim_row = &context.prim_row; + + let constraint = context + .queries + .intern_type(Type::Constructor(prim_row.file_id, prim_row.union)); + let constraint = context.intern_application(constraint, rest); + let constraint = context.intern_application(constraint, right); + let constraint = context.intern_application(constraint, fresh_tail); + + return Ok(Some(MatchInstance::Match { + constraints: vec![constraint], + equalities: vec![(union, result)], + })); + } + + let union_fields = + left_row.fields.iter().chain(right_row.fields.iter()).cloned().collect_vec(); + + let result = intern_row_value(context, RowType::new(union_fields, right_row.tail)); + + Ok(Some(MatchInstance::Match { + constraints: vec![], + equalities: vec![(union, result)], + })) + } + (_, Some(right_row), Some(union_row)) => { + if right_row.tail.is_some() { + return Ok(Some(MatchInstance::Stuck)); + } + if let Some((remaining, mut equalities)) = + subtract_row_fields(state, context, &union_row.fields, &right_row.fields)? + { + let result = intern_row_value(context, RowType::new(remaining, union_row.tail)); + equalities.push((left, result)); + Ok(Some(MatchInstance::Match { constraints: vec![], equalities })) + } else { + Ok(Some(MatchInstance::Apart)) + } + } + (Some(left_row), _, Some(union_row)) => { + if left_row.tail.is_some() { + return Ok(Some(MatchInstance::Stuck)); + } + if let Some((remaining, mut equalities)) = + subtract_row_fields(state, context, &union_row.fields, &left_row.fields)? + { + let result = intern_row_value(context, RowType::new(remaining, union_row.tail)); + equalities.push((right, result)); + Ok(Some(MatchInstance::Match { constraints: vec![], equalities })) + } else { + Ok(Some(MatchInstance::Apart)) + } + } + _ => Ok(Some(MatchInstance::Stuck)), + } +} + +pub fn match_cons( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[label, a, tail, row] = arguments else { + return Ok(None); + }; + + let label = normalise::normalise(state, context, label)?; + let a = normalise::normalise(state, context, a)?; + let tail = normalise::normalise(state, context, tail)?; + let row = normalise::normalise(state, context, row)?; + + let label_symbol = extract_symbol(state, context, label)?; + let tail_row = extract_row(state, context, tail)?; + let row_row = extract_row(state, context, row)?; + + match (label_symbol, tail_row, row_row) { + (Some(label_value), Some(tail_row), _) => { + let mut fields = vec![RowField { label: label_value, id: a }]; + fields.extend(tail_row.fields.iter().cloned()); + + let result = intern_row_value(context, RowType::new(fields, tail_row.tail)); + + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![(row, result)] })) + } + (Some(label_value), _, Some(row_row)) => { + let mut remaining = vec![]; + let mut found_type = None; + + for field in row_row.fields.iter() { + if field.label == label_value && found_type.is_none() { + found_type = Some(field.id); + } else { + remaining.push(field.clone()); + } + } + + if let Some(field_type) = found_type { + let tail_result = intern_row_value(context, RowType::new(remaining, row_row.tail)); + Ok(Some(MatchInstance::Match { + constraints: vec![], + equalities: vec![(a, field_type), (tail, tail_result)], + })) + } else { + Ok(Some(MatchInstance::Apart)) + } + } + _ => Ok(Some(MatchInstance::Stuck)), + } +} + +pub fn match_lacks( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[label, row] = arguments else { + return Ok(None); + }; + + let label = normalise::normalise(state, context, label)?; + let row = normalise::normalise(state, context, row)?; + + let Some(label_value) = extract_symbol(state, context, label)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let Some(row_row) = extract_row(state, context, row)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let has_label = row_row.fields.iter().any(|field| field.label == label_value); + + if has_label { + Ok(Some(MatchInstance::Apart)) + } else if let Some(tail) = row_row.tail { + if row_row.fields.is_empty() { + return Ok(Some(MatchInstance::Stuck)); + } + + let constraint = context + .queries + .intern_type(Type::Constructor(context.prim_row.file_id, context.prim_row.lacks)); + let constraint = context.intern_application(constraint, label); + let constraint = context.intern_application(constraint, tail); + + Ok(Some(MatchInstance::Match { constraints: vec![constraint], equalities: vec![] })) + } else { + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })) + } +} + +pub fn match_nub( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[original, nubbed] = arguments else { + return Ok(None); + }; + + let original = normalise::normalise(state, context, original)?; + let nubbed = normalise::normalise(state, context, nubbed)?; + + let Some(original_row) = extract_closed_row(state, context, original)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let mut seen = FxHashSet::default(); + let mut fields = vec![]; + + for field in original_row.fields.iter() { + if seen.insert(field.label.clone()) { + fields.push(field.clone()); + } + } + + let result = intern_row_value(context, RowType::new(fields, None)); + Ok(Some(match_equality(state, context, nubbed, result)?)) +} From c64c49bbb030e25458a44be378c091e043aa3f0d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 1 Mar 2026 22:22:36 +0800 Subject: [PATCH 293/386] Add integration tests for Prim.Row --- .../fixtures/checking2/057_prim_row/Main.purs | 76 ++++++++++++++++ .../fixtures/checking2/057_prim_row/Main.snap | 86 +++++++++++++++++++ .../checking2/058_prim_row_apart/Main.purs | 19 ++++ .../checking2/058_prim_row_apart/Main.snap | 33 +++++++ .../checking2/059_prim_row_open/Main.purs | 43 ++++++++++ .../checking2/059_prim_row_open/Main.snap | 70 +++++++++++++++ .../060_prim_row_generalization/Main.purs | 52 +++++++++++ .../060_prim_row_generalization/Main.snap | 55 ++++++++++++ .../061_prim_row_nub_left_bias/Main.purs | 16 ++++ .../061_prim_row_nub_left_bias/Main.snap | 14 +++ .../checking2/062_prim_row_record/Main.purs | 21 +++++ .../checking2/062_prim_row_record/Main.snap | 24 ++++++ .../tests/checking2/generated.rs | 12 +++ 13 files changed, 521 insertions(+) create mode 100644 tests-integration/fixtures/checking2/057_prim_row/Main.purs create mode 100644 tests-integration/fixtures/checking2/057_prim_row/Main.snap create mode 100644 tests-integration/fixtures/checking2/058_prim_row_apart/Main.purs create mode 100644 tests-integration/fixtures/checking2/058_prim_row_apart/Main.snap create mode 100644 tests-integration/fixtures/checking2/059_prim_row_open/Main.purs create mode 100644 tests-integration/fixtures/checking2/059_prim_row_open/Main.snap create mode 100644 tests-integration/fixtures/checking2/060_prim_row_generalization/Main.purs create mode 100644 tests-integration/fixtures/checking2/060_prim_row_generalization/Main.snap create mode 100644 tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.purs create mode 100644 tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.snap create mode 100644 tests-integration/fixtures/checking2/062_prim_row_record/Main.purs create mode 100644 tests-integration/fixtures/checking2/062_prim_row_record/Main.snap diff --git a/tests-integration/fixtures/checking2/057_prim_row/Main.purs b/tests-integration/fixtures/checking2/057_prim_row/Main.purs new file mode 100644 index 00000000..704f61ef --- /dev/null +++ b/tests-integration/fixtures/checking2/057_prim_row/Main.purs @@ -0,0 +1,76 @@ +module Main where + +import Prim.Row as Row +import Type.Proxy (Proxy(..)) + +deriveUnion :: forall u. Row.Union (a :: Int) (b :: String) u => Proxy u +deriveUnion = Proxy + +deriveUnionLeft :: forall l. Row.Union l (b :: String) (a :: Int, b :: String) => Proxy l +deriveUnionLeft = Proxy + +deriveUnionRight :: forall r. Row.Union (a :: Int) r (a :: Int, b :: String) => Proxy r +deriveUnionRight = Proxy + +unionEmptyLeft :: forall u. Row.Union () (a :: Int) u => Proxy u +unionEmptyLeft = Proxy + +unionEmptyRight :: forall u. Row.Union (a :: Int) () u => Proxy u +unionEmptyRight = Proxy + +unionBothEmpty :: forall u. Row.Union () () u => Proxy u +unionBothEmpty = Proxy + +unionMultiple :: forall u. Row.Union (a :: Int, b :: String) (c :: Boolean) u => Proxy u +unionMultiple = Proxy + +deriveCons :: forall row. Row.Cons "name" String () row => Proxy row +deriveCons = Proxy + +deriveTail :: forall tail. Row.Cons "name" String tail (name :: String, age :: Int) => Proxy tail +deriveTail = Proxy + +deriveType :: forall t. Row.Cons "name" t () (name :: String) => Proxy t +deriveType = Proxy + +nestedCons :: forall row. Row.Cons "a" Int (b :: String) row => Proxy row +nestedCons = Proxy + +lacksSimple :: forall r. Row.Lacks "missing" (a :: Int, b :: String) => Proxy r -> Proxy r +lacksSimple = \x -> x + +lacksEmpty :: forall r. Row.Lacks "anything" () => Proxy r -> Proxy r +lacksEmpty = \x -> x + +nubNoDuplicates :: forall nubbed. Row.Nub (a :: Int, b :: String) nubbed => Proxy nubbed +nubNoDuplicates = Proxy + +nubEmpty :: forall nubbed. Row.Nub () nubbed => Proxy nubbed +nubEmpty = Proxy + +solveUnion = + { deriveUnion + , deriveUnionLeft + , deriveUnionRight + , unionEmptyLeft + , unionEmptyRight + , unionBothEmpty + , unionMultiple + } + +solveCons = + { deriveCons + , deriveTail + , deriveType + , nestedCons + } + +solveLacks = + { lacksSimple: lacksSimple Proxy + , lacksEmpty: lacksEmpty Proxy + } + +solveNub = + { nubNoDuplicates + , nubEmpty + } diff --git a/tests-integration/fixtures/checking2/057_prim_row/Main.snap b/tests-integration/fixtures/checking2/057_prim_row/Main.snap new file mode 100644 index 00000000..73d68421 --- /dev/null +++ b/tests-integration/fixtures/checking2/057_prim_row/Main.snap @@ -0,0 +1,86 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +deriveUnion :: + forall (u :: Row Type). + Union @Type ( a :: Int ) ( b :: String ) (u :: Row Type) => Proxy @(Row Type) (u :: Row Type) +deriveUnionLeft :: + forall (l :: Row Type). + Union @Type (l :: Row Type) ( b :: String ) ( a :: Int, b :: String ) => + Proxy @(Row Type) (l :: Row Type) +deriveUnionRight :: + forall (r :: Row Type). + Union @Type ( a :: Int ) (r :: Row Type) ( a :: Int, b :: String ) => + Proxy @(Row Type) (r :: Row Type) +unionEmptyLeft :: + forall (u :: Row Type). + Union @Type () ( a :: Int ) (u :: Row Type) => Proxy @(Row Type) (u :: Row Type) +unionEmptyRight :: + forall (u :: Row Type). + Union @Type ( a :: Int ) () (u :: Row Type) => Proxy @(Row Type) (u :: Row Type) +unionBothEmpty :: + forall (t6 :: Type) (u :: Row (t6 :: Type)). + Union @(t6 :: Type) () () (u :: Row (t6 :: Type)) => + Proxy @(Row (t6 :: Type)) (u :: Row (t6 :: Type)) +unionMultiple :: + forall (u :: Row Type). + Union @Type ( a :: Int, b :: String ) ( c :: Boolean ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +deriveCons :: + forall (row :: Row Type). + Cons @Type "name" String () (row :: Row Type) => Proxy @(Row Type) (row :: Row Type) +deriveTail :: + forall (tail :: Row Type). + Cons @Type "name" String (tail :: Row Type) ( age :: Int, name :: String ) => + Proxy @(Row Type) (tail :: Row Type) +deriveType :: + forall (t :: Type). Cons @Type "name" (t :: Type) () ( name :: String ) => Proxy @Type (t :: Type) +nestedCons :: + forall (row :: Row Type). + Cons @Type "a" Int ( b :: String ) (row :: Row Type) => Proxy @(Row Type) (row :: Row Type) +lacksSimple :: + forall (t13 :: Type) (r :: (t13 :: Type)). + Lacks @Type "missing" ( a :: Int, b :: String ) => + Proxy @(t13 :: Type) (r :: (t13 :: Type)) -> Proxy @(t13 :: Type) (r :: (t13 :: Type)) +lacksEmpty :: + forall (t16 :: Type) (t15 :: Type) (r :: (t16 :: Type)). + Lacks @(t15 :: Type) "anything" () => + Proxy @(t16 :: Type) (r :: (t16 :: Type)) -> Proxy @(t16 :: Type) (r :: (t16 :: Type)) +nubNoDuplicates :: + forall (nubbed :: Row Type). + Nub @Type ( a :: Int, b :: String ) (nubbed :: Row Type) => + Proxy @(Row Type) (nubbed :: Row Type) +nubEmpty :: + forall (t19 :: Type) (nubbed :: Row (t19 :: Type)). + Nub @(t19 :: Type) () (nubbed :: Row (t19 :: Type)) => + Proxy @(Row (t19 :: Type)) (nubbed :: Row (t19 :: Type)) +solveUnion :: + forall (t20 :: Type). + { deriveUnion :: Proxy @(Row Type) ( a :: Int, b :: String ) + , deriveUnionLeft :: Proxy @(Row Type) ( a :: Int ) + , deriveUnionRight :: Proxy @(Row Type) ( b :: String ) + , unionBothEmpty :: Proxy @(Row (t20 :: Type)) () + , unionEmptyLeft :: Proxy @(Row Type) ( a :: Int ) + , unionEmptyRight :: Proxy @(Row Type) ( a :: Int ) + , unionMultiple :: Proxy @(Row Type) ( a :: Int, b :: String, c :: Boolean ) + } +solveCons :: + { deriveCons :: Proxy @(Row Type) ( name :: String ) + , deriveTail :: Proxy @(Row Type) ( age :: Int ) + , deriveType :: Proxy @Type String + , nestedCons :: Proxy @(Row Type) ( a :: Int, b :: String ) + } +solveLacks :: + forall (t24 :: Type) (t23 :: (t24 :: Type)) (t22 :: Type) (t21 :: (t22 :: Type)). + { lacksEmpty :: Proxy @(t24 :: Type) (t23 :: (t24 :: Type)) + , lacksSimple :: Proxy @(t22 :: Type) (t21 :: (t22 :: Type)) + } +solveNub :: + forall (t25 :: Type). + { nubEmpty :: Proxy @(Row (t25 :: Type)) () + , nubNoDuplicates :: Proxy @(Row Type) ( a :: Int, b :: String ) + } + +Types diff --git a/tests-integration/fixtures/checking2/058_prim_row_apart/Main.purs b/tests-integration/fixtures/checking2/058_prim_row_apart/Main.purs new file mode 100644 index 00000000..138fae1e --- /dev/null +++ b/tests-integration/fixtures/checking2/058_prim_row_apart/Main.purs @@ -0,0 +1,19 @@ +module Main where + +import Prim.Row as Row +import Type.Proxy (Proxy(..)) + +unionTypeMismatch :: forall l. Row.Union l (b :: String) (a :: Int, b :: Int) => Proxy l +unionTypeMismatch = Proxy + +consMissingLabel :: forall t. Row.Cons "missing" Int t (a :: Int, b :: String) => Proxy t +consMissingLabel = Proxy + +lacksPresent :: Row.Lacks "b" (a :: Int, b :: String) => Proxy (a :: Int, b :: String) +lacksPresent = Proxy + +forceSolve = + { unionTypeMismatch + , consMissingLabel + , lacksPresent + } diff --git a/tests-integration/fixtures/checking2/058_prim_row_apart/Main.snap b/tests-integration/fixtures/checking2/058_prim_row_apart/Main.snap new file mode 100644 index 00000000..9e2fdb30 --- /dev/null +++ b/tests-integration/fixtures/checking2/058_prim_row_apart/Main.snap @@ -0,0 +1,33 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +unionTypeMismatch :: + forall (l :: Row Type). + Union @Type (l :: Row Type) ( b :: String ) ( a :: Int, b :: Int ) => + Proxy @(Row Type) (l :: Row Type) +consMissingLabel :: + forall (t :: Row Type). + Cons @Type "missing" Int (t :: Row Type) ( a :: Int, b :: String ) => + Proxy @(Row Type) (t :: Row Type) +lacksPresent :: + Lacks @Type "b" ( a :: Int, b :: String ) => Proxy @(Row Type) ( a :: Int, b :: String ) +forceSolve :: + forall (t3 :: Row Type) (t2 :: Row Type). + Union @Type (t3 :: Row Type) ( b :: String ) ( a :: Int, b :: Int ) => + Cons @Type "missing" Int (t2 :: Row Type) ( a :: Int, b :: String ) => + { consMissingLabel :: Proxy @(Row Type) (t2 :: Row Type) + , lacksPresent :: Proxy @(Row Type) ( a :: Int, b :: String ) + , unionTypeMismatch :: Proxy @(Row Type) (t3 :: Row Type) + } + +Types + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(18), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/059_prim_row_open/Main.purs b/tests-integration/fixtures/checking2/059_prim_row_open/Main.purs new file mode 100644 index 00000000..3a8706c6 --- /dev/null +++ b/tests-integration/fixtures/checking2/059_prim_row_open/Main.purs @@ -0,0 +1,43 @@ +module Main where + +import Prim.Row as Row +import Type.Proxy (Proxy(..)) + +openLeft :: forall r u. Row.Union (a :: Int | r) (b :: String) u => Proxy u +openLeft = Proxy + +openRight :: forall r u. Row.Union (a :: Int) (b :: String | r) u => Proxy u +openRight = Proxy + +backwardLeft :: forall l r. Row.Union l (b :: String) (a :: Int, b :: String | r) => Proxy l +backwardLeft = Proxy + +backwardRight :: forall r u. Row.Union (a :: Int) r (a :: Int, b :: String | u) => Proxy r +backwardRight = Proxy + +consOpen :: forall r row. Row.Cons "x" Int (a :: String | r) row => Proxy row +consOpen = Proxy + +decomposeOpen :: forall t tail r. Row.Cons "x" t tail (x :: Int, a :: String | r) => Proxy t +decomposeOpen = Proxy + +extractTail :: forall tail r. Row.Cons "x" Int tail (x :: Int, a :: String | r) => Proxy tail +extractTail = Proxy + +lacksOpen :: forall r. Row.Lacks "missing" (a :: Int, b :: String | r) => Proxy r -> Int +lacksOpen _ = 0 + +lacksPresent :: forall r. Row.Lacks "a" (a :: Int | r) => Proxy r -> Int +lacksPresent _ = 0 + +forceSolve = + { openLeft + , openRight + , backwardLeft + , backwardRight + , consOpen + , decomposeOpen + , extractTail + , lacksOpen: lacksOpen Proxy + , lacksPresent: lacksPresent Proxy + } diff --git a/tests-integration/fixtures/checking2/059_prim_row_open/Main.snap b/tests-integration/fixtures/checking2/059_prim_row_open/Main.snap new file mode 100644 index 00000000..63891d1a --- /dev/null +++ b/tests-integration/fixtures/checking2/059_prim_row_open/Main.snap @@ -0,0 +1,70 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +openLeft :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int | (r :: Row Type) ) ( b :: String ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +openRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) ( b :: String | (r :: Row Type) ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +backwardLeft :: + forall (l :: Row Type) (r :: Row Type). + Union @Type (l :: Row Type) ( b :: String ) ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (l :: Row Type) +backwardRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) (r :: Row Type) ( a :: Int, b :: String | (u :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) +consOpen :: + forall (r :: Row Type) (row :: Row Type). + Cons @Type "x" Int ( a :: String | (r :: Row Type) ) (row :: Row Type) => + Proxy @(Row Type) (row :: Row Type) +decomposeOpen :: + forall (t :: Type) (tail :: Row Type) (r :: Row Type). + Cons @Type "x" (t :: Type) (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @Type (t :: Type) +extractTail :: + forall (tail :: Row Type) (r :: Row Type). + Cons @Type "x" Int (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @(Row Type) (tail :: Row Type) +lacksOpen :: + forall (r :: Row Type). + Lacks @Type "missing" ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) -> Int +lacksPresent :: + forall (r :: Row Type). + Lacks @Type "a" ( a :: Int | (r :: Row Type) ) => Proxy @(Row Type) (r :: Row Type) -> Int +forceSolve :: + forall (t23 :: Row Type) (t22 :: Row Type) (t21 :: Row Type) (t20 :: Row Type) (t19 :: Row Type) + (t18 :: Row Type) (t17 :: Row Type). + Union (t23 :: Row Type) ( b :: String ) (t22 :: Row Type) => + { backwardLeft :: Proxy @(Row Type) ( a :: Int | (t21 :: Row Type) ) + , backwardRight :: Proxy @(Row Type) ( b :: String | (t20 :: Row Type) ) + , consOpen :: Proxy @(Row Type) ( a :: String, x :: Int | (t19 :: Row Type) ) + , decomposeOpen :: Proxy @Type Int + , extractTail :: Proxy @(Row Type) ( a :: String | (t18 :: Row Type) ) + , lacksOpen :: Int + , lacksPresent :: Int + , openLeft :: Proxy @(Row Type) ( a :: Int | (t22 :: Row Type) ) + , openRight :: Proxy @(Row Type) ( a :: Int, b :: String | (t17 :: Row Type) ) + } + +Types + +Errors +CheckError { + kind: AmbiguousConstraint { + constraint: Id(22), + }, + crumbs: [], +} +CheckError { + kind: AmbiguousConstraint { + constraint: Id(23), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.purs b/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.purs new file mode 100644 index 00000000..bbf6e43f --- /dev/null +++ b/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.purs @@ -0,0 +1,52 @@ +module Main where + +import Prim.Row as Prim.Row +import Type.Proxy (Proxy(..)) + +foreign import unsafeCoerce :: forall a b. a -> b + +merge + :: forall r1 r2 r3 r4 + . Prim.Row.Union r1 r2 r3 + => Prim.Row.Nub r3 r4 + => Record r1 + -> Record r2 + -> Record r4 +merge _ _ = unsafeCoerce {} + +a = merge { a: 123 } + +b = a { b: 123 } + +fromUnion + :: forall r1 r2 r3 + . Prim.Row.Union r1 r2 r3 + => Record r3 + -> Int +fromUnion _ = unsafeCoerce 0 + +test = fromUnion + +chainedUnion + :: forall r1 r2 r3 r4 r5 + . Prim.Row.Union r1 r2 r3 + => Prim.Row.Union r3 r4 r5 + => Record r1 + -> Record r5 +chainedUnion _ = unsafeCoerce {} + +testChained = chainedUnion { x: 1 } + +multiMerge + :: forall r1 r2 r3 r4 r5 r6 + . Prim.Row.Union r1 r2 r3 + => Prim.Row.Nub r3 r4 + => Prim.Row.Union r4 r5 r6 + => Record r1 + -> Record r5 + -> Record r6 +multiMerge _ _ = unsafeCoerce {} + +testMulti1 = multiMerge { a: 1 } + +testMulti2 = multiMerge { a: 1 } { b: 2 } diff --git a/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.snap b/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.snap new file mode 100644 index 00000000..46eeaab2 --- /dev/null +++ b/tests-integration/fixtures/checking2/060_prim_row_generalization/Main.snap @@ -0,0 +1,55 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +merge :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) (r4 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Nub @Type (r3 :: Row Type) (r4 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r4 :: Row Type) } +a :: + forall (t8 :: Row Type) (t7 :: Row Type) (t6 :: Row Type). + Nub @Type (t8 :: Row Type) (t7 :: Row Type) => + Union @Type ( a :: Int ) (t6 :: Row Type) (t8 :: Row Type) => + {| (t6 :: Row Type) } -> {| (t7 :: Row Type) } +b :: { a :: Int, b :: Int } +fromUnion :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => {| (r3 :: Row Type) } -> Int +test :: + forall (t14 :: Row Type) (t13 :: Row Type) (t12 :: Row Type). + Union @Type (t14 :: Row Type) (t13 :: Row Type) (t12 :: Row Type) => + {| (t12 :: Row Type) } -> Int +chainedUnion :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) (r4 :: Row Type) (r5 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Union @Type (r3 :: Row Type) (r4 :: Row Type) (r5 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r5 :: Row Type) } +testChained :: + forall (t23 :: Row Type) (t22 :: Row Type) (t21 :: Row Type) (t20 :: Row Type). + Union @Type (t23 :: Row Type) (t22 :: Row Type) (t21 :: Row Type) => + Union @Type ( x :: Int ) (t20 :: Row Type) (t23 :: Row Type) => + {| (t21 :: Row Type) } +multiMerge :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) (r4 :: Row Type) (r5 :: Row Type) + (r6 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Nub @Type (r3 :: Row Type) (r4 :: Row Type) => + Union @Type (r4 :: Row Type) (r5 :: Row Type) (r6 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r5 :: Row Type) } -> {| (r6 :: Row Type) } +testMulti1 :: + forall (t34 :: Row Type) (t33 :: Row Type) (t32 :: Row Type) (t31 :: Row Type) (t30 :: Row Type). + Nub @Type (t34 :: Row Type) (t33 :: Row Type) => + Union @Type (t33 :: Row Type) (t32 :: Row Type) (t31 :: Row Type) => + Union @Type ( a :: Int ) (t30 :: Row Type) (t34 :: Row Type) => + {| (t32 :: Row Type) } -> {| (t31 :: Row Type) } +testMulti2 :: + forall (t38 :: Row Type) (t37 :: Row Type) (t36 :: Row Type) (t35 :: Row Type). + Nub @Type (t38 :: Row Type) (t37 :: Row Type) => + Union @Type ( a :: Int ) (t36 :: Row Type) (t38 :: Row Type) => + Union @Type (t37 :: Row Type) ( b :: Int ) (t35 :: Row Type) => + {| (t35 :: Row Type) } + +Types diff --git a/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.purs b/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.purs new file mode 100644 index 00000000..ea387c53 --- /dev/null +++ b/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.purs @@ -0,0 +1,16 @@ +module Main where + +import Prim.Row as Prim.Row + +foreign import unsafeCoerce :: forall a b. a -> b + +merge + :: forall r1 r2 r3 r4 + . Prim.Row.Union r1 r2 r3 + => Prim.Row.Nub r3 r4 + => Record r1 + -> Record r2 + -> Record r4 +merge _ _ = unsafeCoerce {} + +test = merge { a: 42, b: "life" } { b: 42 } diff --git a/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.snap b/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.snap new file mode 100644 index 00000000..2dd9363b --- /dev/null +++ b/tests-integration/fixtures/checking2/061_prim_row_nub_left_bias/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +merge :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) (r4 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Nub @Type (r3 :: Row Type) (r4 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r4 :: Row Type) } +test :: { a :: Int, b :: String } + +Types diff --git a/tests-integration/fixtures/checking2/062_prim_row_record/Main.purs b/tests-integration/fixtures/checking2/062_prim_row_record/Main.purs new file mode 100644 index 00000000..9ab0137a --- /dev/null +++ b/tests-integration/fixtures/checking2/062_prim_row_record/Main.purs @@ -0,0 +1,21 @@ +module Main where + +import Prim.Row as Row + +foreign import unsafeCoerce :: forall a b. a -> b + +union :: forall r1 r2 r3. Row.Union r1 r2 r3 => Record r1 -> Record r2 -> Record r3 +union _ _ = unsafeCoerce {} + +addField :: forall r. { a :: Int | r } -> { a :: Int, b :: String | r } +addField x = union x { b: "hi" } + +test = addField { a: 1, c: true } + +insertX :: forall r. Row.Lacks "x" r => Record r -> Record (x :: Int | r) +insertX _ = unsafeCoerce {} + +insertOpen :: forall r. Row.Lacks "x" r => { a :: Int | r } -> { x :: Int, a :: Int | r } +insertOpen x = insertX x + +test2 = insertOpen { a: 1, b: "hi" } diff --git a/tests-integration/fixtures/checking2/062_prim_row_record/Main.snap b/tests-integration/fixtures/checking2/062_prim_row_record/Main.snap new file mode 100644 index 00000000..ccc5db30 --- /dev/null +++ b/tests-integration/fixtures/checking2/062_prim_row_record/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +union :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r3 :: Row Type) } +addField :: + forall (r :: Row Type). + { a :: Int | (r :: Row Type) } -> { a :: Int, b :: String | (r :: Row Type) } +test :: { a :: Int, b :: String, c :: Boolean } +insertX :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => {| (r :: Row Type) } -> { x :: Int | (r :: Row Type) } +insertOpen :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => + { a :: Int | (r :: Row Type) } -> { a :: Int, x :: Int | (r :: Row Type) } +test2 :: { a :: Int, b :: String, x :: Int } + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 85f1a24a..aed8ea3c 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -141,3 +141,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_055_prim_solver_apart_main() { run_test("055_prim_solver_apart", "Main"); } #[rustfmt::skip] #[test] fn test_056_prim_row_list_main() { run_test("056_prim_row_list", "Main"); } + +#[rustfmt::skip] #[test] fn test_057_prim_row_main() { run_test("057_prim_row", "Main"); } + +#[rustfmt::skip] #[test] fn test_058_prim_row_apart_main() { run_test("058_prim_row_apart", "Main"); } + +#[rustfmt::skip] #[test] fn test_059_prim_row_open_main() { run_test("059_prim_row_open", "Main"); } + +#[rustfmt::skip] #[test] fn test_060_prim_row_generalization_main() { run_test("060_prim_row_generalization", "Main"); } + +#[rustfmt::skip] #[test] fn test_061_prim_row_nub_left_bias_main() { run_test("061_prim_row_nub_left_bias", "Main"); } + +#[rustfmt::skip] #[test] fn test_062_prim_row_record_main() { run_test("062_prim_row_record", "Main"); } From 84c4d1657298ea985d1d2a776cd9730fafa07994 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 00:53:59 +0800 Subject: [PATCH 294/386] Implement Reflectable and Prim.TypeError --- .../checking2/src/core/constraint/compiler.rs | 12 +- .../constraint/compiler/prim_reflectable.rs | 69 +++++++ .../constraint/compiler/prim_type_error.rs | 176 ++++++++++++++++++ 3 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index c39b394f..50260559 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -1,7 +1,9 @@ mod prim_int; +mod prim_reflectable; mod prim_row; mod prim_row_list; mod prim_symbol; +mod prim_type_error; use building_types::QueryResult; use smol_str::SmolStr; @@ -147,11 +149,17 @@ where } else if *file_id == context.prim_coerce.file_id { None } else if *file_id == context.prim_type_error.file_id { - None + if *item_id == context.prim_type_error.warn { + prim_type_error::match_warn(state, context, arguments)? + } else if *item_id == context.prim_type_error.fail { + prim_type_error::match_fail(state, context, arguments)? + } else { + None + } } else if context.known_reflectable.is_symbol == Some((*file_id, *item_id)) { prim_symbol::match_is_symbol(state, context, arguments)? } else if context.known_reflectable.reflectable == Some((*file_id, *item_id)) { - None + prim_reflectable::match_reflectable(state, context, arguments)? } else { None }; diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs new file mode 100644 index 00000000..e084d4d4 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs @@ -0,0 +1,69 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::unification::{CanUnify, can_unify}; +use crate::core::{Type, TypeId, normalise}; +use crate::state::CheckState; + +use super::{extract_integer, extract_symbol}; + +pub fn match_reflectable( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[v, t] = arguments else { return Ok(None) }; + + let v = normalise::normalise(state, context, v)?; + let t = normalise::normalise(state, context, t)?; + + if extract_symbol(state, context, v)?.is_some() { + return Ok(Some(match_expected(state, context, t, context.prim.string)?)); + } + + if extract_integer(state, context, v)?.is_some() { + return Ok(Some(match_expected(state, context, t, context.prim.int)?)); + } + + if v == context.prim_boolean.true_ || v == context.prim_boolean.false_ { + return Ok(Some(match_expected(state, context, t, context.prim.boolean)?)); + } + + if v == context.prim_ordering.lt + || v == context.prim_ordering.eq + || v == context.prim_ordering.gt + { + let Some(expected) = context.known_reflectable.ordering else { + return Ok(Some(MatchInstance::Stuck)); + }; + return Ok(Some(match_expected(state, context, t, expected)?)); + } + + if matches!(context.lookup_type(v), Type::Unification(_)) { + return Ok(Some(MatchInstance::Stuck)); + } + + Ok(Some(MatchInstance::Apart)) +} + +fn match_expected( + state: &mut CheckState, + context: &CheckContext, + actual: TypeId, + expected: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + Ok(match can_unify(state, context, actual, expected)? { + CanUnify::Apart => MatchInstance::Apart, + CanUnify::Equal | CanUnify::Unify => { + MatchInstance::Match { constraints: vec![], equalities: vec![(actual, expected)] } + } + }) +} diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs new file mode 100644 index 00000000..fef66c2b --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs @@ -0,0 +1,176 @@ +use building_types::QueryResult; +use lowering::StringKind; +use smol_str::{SmolStr, format_smolstr}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::pretty::Pretty; +use crate::core::{Type, TypeId, normalise, toolkit, zonk}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +fn is_stuck(state: &mut CheckState, context: &CheckContext, id: TypeId) -> QueryResult +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + Ok(matches!(context.lookup_type(id), Type::Unification(_))) +} + +fn extract_symbol_text( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + if let Type::String(_, smol_str_id) = context.lookup_type(id) { + Ok(Some(context.queries.lookup_smol_str(smol_str_id))) + } else { + Ok(None) + } +} + +fn extract_symbol_with_kind( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + if let Type::String(kind, smol_id) = context.lookup_type(id) { + Ok(Some((kind, context.queries.lookup_smol_str(smol_id)))) + } else { + Ok(None) + } +} + +fn is_valid_label(s: &str) -> bool { + let mut chars = s.chars(); + match chars.next() { + Some(c) if c.is_lowercase() || c == '_' => {} + _ => return false, + } + chars.all(|c| c.is_alphanumeric() || c == '_' || c == '\'') +} + +fn render_label(kind: StringKind, text: &str) -> SmolStr { + if is_valid_label(text) { + SmolStr::new(text) + } else { + match kind { + StringKind::String => SmolStr::new(format!(r#""{text}""#)), + StringKind::RawString => SmolStr::new(format!(r#""""{text}""""#)), + } + } +} + +/// Renders a `Doc` type into a string for custom type error messages. +/// +/// Returns `None` if the doc is stuck on an unsolved unification variable, +/// signalling that the constraint solver should return `Stuck`. +fn render_doc( + state: &mut CheckState, + context: &CheckContext, + doc: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let doc = normalise::normalise(state, context, doc)?; + + if matches!(context.lookup_type(doc), Type::Unification(_)) { + return Ok(None); + } + + let (constructor, arguments) = toolkit::extract_type_application(state, context, doc)?; + let prim = &context.prim_type_error; + + if constructor == prim.text { + let &[symbol] = arguments.as_slice() else { return Ok(None) }; + if is_stuck(state, context, symbol)? { + return Ok(None); + } + extract_symbol_text(state, context, symbol) + } else if constructor == prim.quote { + let &[quoted_type] = arguments.as_slice() else { return Ok(None) }; + if is_stuck(state, context, quoted_type)? { + return Ok(None); + } + let quoted_type = zonk::zonk(state, context, quoted_type)?; + let rendered = Pretty::new(context.queries, &state.checked).render(quoted_type); + Ok(Some(rendered)) + } else if constructor == prim.quote_label { + let &[symbol] = arguments.as_slice() else { return Ok(None) }; + if is_stuck(state, context, symbol)? { + return Ok(None); + } + Ok(extract_symbol_with_kind(state, context, symbol)? + .map(|(kind, text)| render_label(kind, &text))) + } else if constructor == prim.beside { + let &[left, right] = arguments.as_slice() else { return Ok(None) }; + let Some(left_rendered) = render_doc(state, context, left)? else { + return Ok(None); + }; + let Some(right_rendered) = render_doc(state, context, right)? else { + return Ok(None); + }; + Ok(Some(format_smolstr!("{left_rendered}{right_rendered}"))) + } else if constructor == prim.above { + let &[upper, lower] = arguments.as_slice() else { return Ok(None) }; + let Some(upper_rendered) = render_doc(state, context, upper)? else { + return Ok(None); + }; + let Some(lower_rendered) = render_doc(state, context, lower)? else { + return Ok(None); + }; + Ok(Some(format_smolstr!("{upper_rendered}\n{lower_rendered}"))) + } else { + Ok(None) + } +} + +pub fn match_warn( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[doc] = arguments else { return Ok(None) }; + + let Some(message) = render_doc(state, context, doc)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let message_id = context.queries.intern_smol_str(message); + state.insert_error(ErrorKind::CustomWarning { message_id }); + + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })) +} + +pub fn match_fail( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[doc] = arguments else { return Ok(None) }; + + let Some(message) = render_doc(state, context, doc)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + let message_id = context.queries.intern_smol_str(message); + state.insert_error(ErrorKind::CustomFailure { message_id }); + + Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })) +} From 16b2aa2635498965abf5b82346162ff9c6f4a224 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 00:53:59 +0800 Subject: [PATCH 295/386] Add tests for Reflectable and Prim.TypeError --- .../checking2/063_prim_reflectable/Main.purs | 42 ++++++ .../checking2/063_prim_reflectable/Main.snap | 22 +++ .../064_prim_type_error_warn/Main.purs | 54 ++++++++ .../064_prim_type_error_warn/Main.snap | 125 ++++++++++++++++++ .../065_prim_type_error_fail/Main.purs | 18 +++ .../065_prim_type_error_fail/Main.snap | 44 ++++++ .../tests/checking2/generated.rs | 6 + 7 files changed, 311 insertions(+) create mode 100644 tests-integration/fixtures/checking2/063_prim_reflectable/Main.purs create mode 100644 tests-integration/fixtures/checking2/063_prim_reflectable/Main.snap create mode 100644 tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.purs create mode 100644 tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.snap create mode 100644 tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.purs create mode 100644 tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.snap diff --git a/tests-integration/fixtures/checking2/063_prim_reflectable/Main.purs b/tests-integration/fixtures/checking2/063_prim_reflectable/Main.purs new file mode 100644 index 00000000..19958383 --- /dev/null +++ b/tests-integration/fixtures/checking2/063_prim_reflectable/Main.purs @@ -0,0 +1,42 @@ +module Main where + +import Type.Proxy (Proxy(..)) +import Data.Reflectable (class Reflectable, reflectType) +import Data.Ordering (Ordering) +import Prim.Boolean (True, False) +import Prim.Ordering (LT, EQ, GT) + +testSymbol :: String +testSymbol = reflectType (Proxy :: Proxy "hello") + +testSymbol' = reflectType (Proxy :: Proxy "hello") + +testInt :: Int +testInt = reflectType (Proxy :: Proxy 42) + +testInt' = reflectType (Proxy :: Proxy 42) + +testTrue :: Boolean +testTrue = reflectType (Proxy :: Proxy True) + +testTrue' = reflectType (Proxy :: Proxy True) + +testFalse :: Boolean +testFalse = reflectType (Proxy :: Proxy False) + +testFalse' = reflectType (Proxy :: Proxy False) + +testLT :: Ordering +testLT = reflectType (Proxy :: Proxy LT) + +testLT' = reflectType (Proxy :: Proxy LT) + +testEQ :: Ordering +testEQ = reflectType (Proxy :: Proxy EQ) + +testEQ' = reflectType (Proxy :: Proxy EQ) + +testGT :: Ordering +testGT = reflectType (Proxy :: Proxy GT) + +testGT' = reflectType (Proxy :: Proxy GT) diff --git a/tests-integration/fixtures/checking2/063_prim_reflectable/Main.snap b/tests-integration/fixtures/checking2/063_prim_reflectable/Main.snap new file mode 100644 index 00000000..71bb0245 --- /dev/null +++ b/tests-integration/fixtures/checking2/063_prim_reflectable/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testSymbol :: String +testSymbol' :: String +testInt :: Int +testInt' :: Int +testTrue :: Boolean +testTrue' :: Boolean +testFalse :: Boolean +testFalse' :: Boolean +testLT :: Ordering +testLT' :: Ordering +testEQ :: Ordering +testEQ' :: Ordering +testGT :: Ordering +testGT' :: Ordering + +Types diff --git a/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.purs b/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.purs new file mode 100644 index 00000000..d5447949 --- /dev/null +++ b/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.purs @@ -0,0 +1,54 @@ +module Main where + +import Prim.TypeError (class Warn, Text, Quote, QuoteLabel, Beside, Above) + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +warnBasic :: forall a. Warn (Text "This function is deprecated") => a -> a +warnBasic x = x + +useWarnBasic :: Int +useWarnBasic = warnBasic 42 + +warnBeside :: forall a. Warn (Beside (Text "Left ") (Text "Right")) => a -> a +warnBeside x = x + +useWarnBeside :: Int +useWarnBeside = warnBeside 42 + +warnAbove :: forall a. Warn (Above (Text "Line 1") (Text "Line 2")) => a -> a +warnAbove x = x + +useWarnAbove :: Int +useWarnAbove = warnAbove 42 + +warnQuote :: forall a. Warn (Beside (Text "Got type: ") (Quote a)) => Proxy a -> Proxy a +warnQuote p = p + +useWarnQuote :: Proxy Int +useWarnQuote = warnQuote Proxy + +warnQuoteLabel :: forall a. Warn (Beside (Text "Label: ") (QuoteLabel "myField")) => a -> a +warnQuoteLabel x = x + +useWarnQuoteLabel :: Int +useWarnQuoteLabel = warnQuoteLabel 42 + +warnQuoteLabelSpaces :: forall a. Warn (Beside (Text "Label: ") (QuoteLabel "h e l l o")) => a -> a +warnQuoteLabelSpaces x = x + +useWarnQuoteLabelSpaces :: Int +useWarnQuoteLabelSpaces = warnQuoteLabelSpaces 42 + +warnQuoteLabelQuote :: forall a. Warn (Beside (Text "Label: ") (QuoteLabel "hel\"lo")) => a -> a +warnQuoteLabelQuote x = x + +useWarnQuoteLabelQuote :: Int +useWarnQuoteLabelQuote = warnQuoteLabelQuote 42 + +warnQuoteLabelRaw :: forall a. Warn (Beside (Text "Label: ") (QuoteLabel """raw\nstring""")) => a -> a +warnQuoteLabelRaw x = x + +useWarnQuoteLabelRaw :: Int +useWarnQuoteLabelRaw = warnQuoteLabelRaw 42 diff --git a/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.snap b/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.snap new file mode 100644 index 00000000..727e1624 --- /dev/null +++ b/tests-integration/fixtures/checking2/064_prim_type_error_warn/Main.snap @@ -0,0 +1,125 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +warnBasic :: + forall (a :: Type). Warn (Text "This function is deprecated") => (a :: Type) -> (a :: Type) +useWarnBasic :: Int +warnBeside :: + forall (a :: Type). Warn (Beside (Text "Left ") (Text "Right")) => (a :: Type) -> (a :: Type) +useWarnBeside :: Int +warnAbove :: + forall (a :: Type). Warn (Above (Text "Line 1") (Text "Line 2")) => (a :: Type) -> (a :: Type) +useWarnAbove :: Int +warnQuote :: + forall (t6 :: Type) (a :: (t6 :: Type)). + Warn (Beside (Text "Got type: ") (Quote @(t6 :: Type) (a :: (t6 :: Type)))) => + Proxy @(t6 :: Type) (a :: (t6 :: Type)) -> Proxy @(t6 :: Type) (a :: (t6 :: Type)) +useWarnQuote :: Proxy @Type Int +warnQuoteLabel :: + forall (a :: Type). + Warn (Beside (Text "Label: ") (QuoteLabel "myField")) => (a :: Type) -> (a :: Type) +useWarnQuoteLabel :: Int +warnQuoteLabelSpaces :: + forall (a :: Type). + Warn (Beside (Text "Label: ") (QuoteLabel "h e l l o")) => (a :: Type) -> (a :: Type) +useWarnQuoteLabelSpaces :: Int +warnQuoteLabelQuote :: + forall (a :: Type). + Warn (Beside (Text "Label: ") (QuoteLabel "hel\"lo")) => (a :: Type) -> (a :: Type) +useWarnQuoteLabelQuote :: Int +warnQuoteLabelRaw :: + forall (a :: Type). + Warn (Beside (Text "Label: ") (QuoteLabel """raw\nstring""")) => (a :: Type) -> (a :: Type) +useWarnQuoteLabelRaw :: Int + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] + +Errors +CheckError { + kind: CustomWarning { + message_id: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(17), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(20), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(22), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(25), + }, + crumbs: [ + TermDeclaration( + Idx::(10), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(27), + }, + crumbs: [ + TermDeclaration( + Idx::(12), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(29), + }, + crumbs: [ + TermDeclaration( + Idx::(14), + ), + ], +} +CheckError { + kind: CustomWarning { + message_id: Id(31), + }, + crumbs: [ + TermDeclaration( + Idx::(16), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.purs b/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.purs new file mode 100644 index 00000000..59995c57 --- /dev/null +++ b/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.purs @@ -0,0 +1,18 @@ +module Main where + +import Prim.TypeError (class Fail, Text, Quote, Beside, Above) + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +failBasic :: forall a. Fail (Text "This operation is not allowed") => a -> a +failBasic x = x + +useFailBasic :: Int +useFailBasic = failBasic 42 + +failComplex :: forall a. Fail (Above (Text "Error:") (Beside (Text "Type ") (Quote a))) => Proxy a -> Proxy a +failComplex p = p + +useFailComplex :: Proxy String +useFailComplex = failComplex Proxy diff --git a/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.snap b/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.snap new file mode 100644 index 00000000..4b774ff5 --- /dev/null +++ b/tests-integration/fixtures/checking2/065_prim_type_error_fail/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +failBasic :: + forall (a :: Type). Fail (Text "This operation is not allowed") => (a :: Type) -> (a :: Type) +useFailBasic :: Int +failComplex :: + forall (t4 :: Type) (a :: (t4 :: Type)). + Fail + (Above (Text "Error:") (Beside (Text "Type ") (Quote @(t4 :: Type) (a :: (t4 :: Type))))) => + Proxy @(t4 :: Type) (a :: (t4 :: Type)) -> Proxy @(t4 :: Type) (a :: (t4 :: Type)) +useFailComplex :: Proxy @Type String + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] + +Errors +CheckError { + kind: CustomFailure { + message_id: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: CustomFailure { + message_id: Id(17), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index aed8ea3c..34b4f93f 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -153,3 +153,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_061_prim_row_nub_left_bias_main() { run_test("061_prim_row_nub_left_bias", "Main"); } #[rustfmt::skip] #[test] fn test_062_prim_row_record_main() { run_test("062_prim_row_record", "Main"); } + +#[rustfmt::skip] #[test] fn test_063_prim_reflectable_main() { run_test("063_prim_reflectable", "Main"); } + +#[rustfmt::skip] #[test] fn test_064_prim_type_error_warn_main() { run_test("064_prim_type_error_warn", "Main"); } + +#[rustfmt::skip] #[test] fn test_065_prim_type_error_fail_main() { run_test("065_prim_type_error_fail", "Main"); } From bae63e4dd961aed86642ffb1e66b9e5c134e56ac Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 01:11:40 +0800 Subject: [PATCH 296/386] Add tests for givens and minimisation --- .../Main.purs | 23 +++++++++++++ .../Main.snap | 21 ++++++++++++ .../Main.purs | 32 +++++++++++++++++++ .../Main.snap | 25 +++++++++++++++ .../tests/checking2/generated.rs | 4 +++ 5 files changed, 105 insertions(+) create mode 100644 tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.purs create mode 100644 tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.snap create mode 100644 tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.purs create mode 100644 tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.snap diff --git a/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.purs b/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.purs new file mode 100644 index 00000000..c2e30958 --- /dev/null +++ b/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.purs @@ -0,0 +1,23 @@ +module Main where + +import Prim.Row as Row + +foreign import unsafeCoerce :: forall a b. a -> b + +class Row.Union r1 r2 r3 <= Merge r1 r2 r3 + +merge + :: forall r1 r2 r3 + . Row.Union r1 r2 r3 + => Record r1 + -> Record r2 + -> Record r3 +merge _ _ = unsafeCoerce {} + +merge2 + :: forall r1 r2 r3 + . Merge r1 r2 r3 + => Record r1 + -> Record r2 + -> Record r3 +merge2 r1 r2 = merge r1 r2 diff --git a/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.snap b/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.snap new file mode 100644 index 00000000..50c3e009 --- /dev/null +++ b/tests-integration/fixtures/checking2/066_compiler_solved_superclass_given/Main.snap @@ -0,0 +1,21 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +merge :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r3 :: Row Type) } +merge2 :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Merge @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r3 :: Row Type) } + +Types +Merge :: forall (t3 :: Type). Row (t3 :: Type) -> Row (t3 :: Type) -> Row (t3 :: Type) -> Constraint + +Classes +class forall (t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)). Union @(t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)) <= Merge @(t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)) diff --git a/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.purs b/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.purs new file mode 100644 index 00000000..766270c0 --- /dev/null +++ b/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.purs @@ -0,0 +1,32 @@ +module Main where + +import Prim.Row as Row +import Type.Proxy (Proxy(..)) + +foreign import unsafeCoerce :: forall a b. a -> b + +class Row.Union r1 r2 r3 <= Merge r1 r2 r3 + +useMerge + :: forall r1 r2 r3 + . Merge r1 r2 r3 + => Proxy r3 + -> Record r1 + -> Record r2 + -> Int +useMerge _ _ _ = unsafeCoerce 0 + +useUnion + :: forall r1 r2 r3 + . Row.Union r1 r2 r3 + => Proxy r3 + -> Record r1 + -> Record r2 + -> Int +useUnion _ _ _ = unsafeCoerce 0 + +testMin p r1 r2 = + let + _ = useMerge p r1 r2 + in + useUnion p r1 r2 diff --git a/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.snap b/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.snap new file mode 100644 index 00000000..8f272560 --- /dev/null +++ b/tests-integration/fixtures/checking2/067_compiler_solved_superclass_minimisation/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +useMerge :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Merge @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Proxy @(Row Type) (r3 :: Row Type) -> {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> Int +useUnion :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + Proxy @(Row Type) (r3 :: Row Type) -> {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> Int +testMin :: + forall (t14 :: Row Type) (t13 :: Row Type) (t12 :: Row Type). + Merge @Type (t14 :: Row Type) (t13 :: Row Type) (t12 :: Row Type) => + Proxy @(Row Type) (t12 :: Row Type) -> {| (t14 :: Row Type) } -> {| (t13 :: Row Type) } -> Int + +Types +Merge :: forall (t3 :: Type). Row (t3 :: Type) -> Row (t3 :: Type) -> Row (t3 :: Type) -> Constraint + +Classes +class forall (t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)). Union @(t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)) <= Merge @(t3 :: Type) (r1 :: Row (t3 :: Type)) (r2 :: Row (t3 :: Type)) (r3 :: Row (t3 :: Type)) diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 34b4f93f..34203718 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -159,3 +159,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_064_prim_type_error_warn_main() { run_test("064_prim_type_error_warn", "Main"); } #[rustfmt::skip] #[test] fn test_065_prim_type_error_fail_main() { run_test("065_prim_type_error_fail", "Main"); } + +#[rustfmt::skip] #[test] fn test_066_compiler_solved_superclass_given_main() { run_test("066_compiler_solved_superclass_given", "Main"); } + +#[rustfmt::skip] #[test] fn test_067_compiler_solved_superclass_minimisation_main() { run_test("067_compiler_solved_superclass_minimisation", "Main"); } From fd4c1fb963b145b1534ee95d42f3d06002e1bb4a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 01:31:26 +0800 Subject: [PATCH 297/386] Implemet Prim.Coerce instances --- .../checking2/src/core/constraint/compiler.rs | 7 +- .../core/constraint/compiler/prim_coerce.rs | 478 ++++++++++++++++++ compiler-core/checking2/src/core/toolkit.rs | 92 ++++ 3 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index 50260559..3269c1b3 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -1,3 +1,4 @@ +mod prim_coerce; mod prim_int; mod prim_reflectable; mod prim_row; @@ -147,7 +148,11 @@ where None } } else if *file_id == context.prim_coerce.file_id { - None + if *item_id == context.prim_coerce.coercible { + prim_coerce::match_coercible(state, context, arguments)? + } else { + None + } } else if *file_id == context.prim_type_error.file_id { if *item_id == context.prim_type_error.warn { prim_type_error::match_warn(state, context, arguments)? diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs new file mode 100644 index 00000000..d2a12085 --- /dev/null +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -0,0 +1,478 @@ +use std::sync::Arc; + +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use itertools::izip; + +use crate::context::CheckContext; +use crate::core::constraint::MatchInstance; +use crate::core::substitute::SubstituteName; +use crate::core::unification::{CanUnify, can_unify}; +use crate::core::{Role, Type, TypeId, normalise, toolkit}; +use crate::error::ErrorKind; +use crate::source::types; +use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; + +enum NewtypeCoercionResult { + Success(MatchInstance), + ConstructorNotInScope { file_id: FileId, item_id: TypeItemId }, + NotApplicable, +} + +pub fn match_coercible( + state: &mut CheckState, + context: &CheckContext, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let &[left, right] = arguments else { + return Ok(None); + }; + + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + + if left == right { + return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); + } + + if is_unification_head(state, context, left)? || is_unification_head(state, context, right)? { + return Ok(Some(MatchInstance::Stuck)); + } + + if try_refl(state, context, left, right)? { + return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); + } + + let newtype_result = try_newtype_coercion(state, context, left, right)?; + if let NewtypeCoercionResult::Success(result) = newtype_result { + return Ok(Some(result)); + } + + if let Some(result) = try_application_coercion(state, context, left, right)? { + return Ok(Some(result)); + } + + if let Some(result) = try_function_coercion(state, context, left, right)? { + return Ok(Some(result)); + } + + if let Some(result) = try_higher_kinded_coercion(state, context, left, right)? { + return Ok(Some(result)); + } + + if let Some(result) = try_row_coercion(state, context, left, right)? { + return Ok(Some(result)); + } + + if let NewtypeCoercionResult::ConstructorNotInScope { file_id, item_id } = newtype_result { + state.insert_error(ErrorKind::CoercibleConstructorNotInScope { file_id, item_id }); + } + + Ok(Some(MatchInstance::Apart)) +} + +fn is_unification_head( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Unification(_) => return Ok(true), + Type::Application(function, _) | Type::KindApplication(function, _) => { + id = function; + } + _ => return Ok(false), + } + } +} + +fn has_type_kind( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let kind = types::elaborate_kind(state, context, id)?; + let kind = normalise::normalise(state, context, kind)?; + Ok(kind == context.prim.t) +} + +fn try_newtype_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut hidden_newtype: Option<(FileId, TypeItemId)> = None; + + if has_type_kind(state, context, left)? + && let Some((file_id, item_id)) = toolkit::extract_type_constructor(state, context, left)? + && toolkit::is_newtype(context, file_id, item_id)? + { + if toolkit::is_constructor_in_scope(context, file_id, item_id)? { + if let Some(inner) = toolkit::get_newtype_inner(state, context, file_id, item_id, left)? + { + let constraint = make_coercible_constraint(context, inner, right); + return Ok(NewtypeCoercionResult::Success(MatchInstance::Match { + constraints: vec![constraint], + equalities: vec![], + })); + } + } else { + hidden_newtype = Some((file_id, item_id)); + } + } + + if has_type_kind(state, context, right)? + && let Some((file_id, item_id)) = toolkit::extract_type_constructor(state, context, right)? + && toolkit::is_newtype(context, file_id, item_id)? + { + if toolkit::is_constructor_in_scope(context, file_id, item_id)? { + if let Some(inner) = + toolkit::get_newtype_inner(state, context, file_id, item_id, right)? + { + let constraint = make_coercible_constraint(context, left, inner); + return Ok(NewtypeCoercionResult::Success(MatchInstance::Match { + constraints: vec![constraint], + equalities: vec![], + })); + } + } else if hidden_newtype.is_none() { + hidden_newtype = Some((file_id, item_id)); + } + } + + if let Some((file_id, item_id)) = hidden_newtype { + return Ok(NewtypeCoercionResult::ConstructorNotInScope { file_id, item_id }); + } + + Ok(NewtypeCoercionResult::NotApplicable) +} + +fn try_application_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some((left_file, left_id)) = toolkit::extract_type_constructor(state, context, left)? + else { + return Ok(None); + }; + let Some((right_file, right_id)) = toolkit::extract_type_constructor(state, context, right)? + else { + return Ok(None); + }; + + if left_file != right_file || left_id != right_id { + return Ok(None); + } + + let (_, left_arguments) = toolkit::extract_type_application(state, context, left)?; + let (_, right_arguments) = toolkit::extract_type_application(state, context, right)?; + + if left_arguments.len() != right_arguments.len() { + return Ok(Some(MatchInstance::Apart)); + } + + let Some(roles) = toolkit::lookup_file_roles(state, context, left_file, left_id)? else { + return Ok(Some(MatchInstance::Stuck)); + }; + + debug_assert_eq!(roles.len(), left_arguments.len(), "critical failure: mismatched lengths"); + debug_assert_eq!(roles.len(), right_arguments.len(), "critical failure: mismatched lengths"); + + let mut constraints = vec![]; + let mut equalities = vec![]; + + for (role, &left_argument, &right_argument) in izip!(&*roles, &left_arguments, &right_arguments) + { + match role { + Role::Phantom => (), + Role::Representational => { + let constraint = make_coercible_constraint(context, left_argument, right_argument); + constraints.push(constraint); + } + Role::Nominal => { + if left_argument != right_argument { + if can_unify(state, context, left_argument, right_argument)? == CanUnify::Apart + { + return Ok(Some(MatchInstance::Apart)); + } + equalities.push((left_argument, right_argument)); + } + } + } + } + + Ok(Some(MatchInstance::Match { constraints, equalities })) +} + +fn try_function_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let left_function = decompose_function_simple(state, context, left)?; + let right_function = decompose_function_simple(state, context, right)?; + + let (Some((left_argument, left_result)), Some((right_argument, right_result))) = + (left_function, right_function) + else { + return Ok(None); + }; + + let left_constraint = make_coercible_constraint(context, left_argument, right_argument); + let right_constraint = make_coercible_constraint(context, left_result, right_result); + + Ok(Some(MatchInstance::Match { + constraints: vec![left_constraint, right_constraint], + equalities: vec![], + })) +} + +fn decompose_function_simple( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Function(argument, result) => Ok(Some((argument, result))), + Type::Application(partial, result) => { + let partial = normalise::normalise(state, context, partial)?; + if let Type::Application(constructor, argument) = context.lookup_type(partial) { + let constructor = normalise::normalise(state, context, constructor)?; + if constructor == context.prim.function { + return Ok(Some((argument, result))); + } + } + Ok(None) + } + _ => Ok(None), + } +} + +fn try_row_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let left = normalise::normalise(state, context, left)?; + let right = normalise::normalise(state, context, right)?; + + let Type::Row(left_row_id) = context.lookup_type(left) else { return Ok(None) }; + let Type::Row(right_row_id) = context.lookup_type(right) else { return Ok(None) }; + + let left_row = context.lookup_row_type(left_row_id); + let right_row = context.lookup_row_type(right_row_id); + + if left_row.fields.len() != right_row.fields.len() { + return Ok(Some(MatchInstance::Apart)); + } + + let mut constraints = vec![]; + + for (left_field, right_field) in izip!(&*left_row.fields, &*right_row.fields) { + if left_field.label != right_field.label { + return Ok(Some(MatchInstance::Apart)); + } + let constraint = make_coercible_constraint(context, left_field.id, right_field.id); + constraints.push(constraint); + } + + match (left_row.tail, right_row.tail) { + (None, None) => (), + (Some(left_tail), Some(right_tail)) => { + let constraint = make_coercible_constraint(context, left_tail, right_tail); + constraints.push(constraint); + } + (None, Some(_)) | (Some(_), None) => { + return Ok(Some(MatchInstance::Apart)); + } + } + + Ok(Some(MatchInstance::Match { constraints, equalities: vec![] })) +} + +fn try_higher_kinded_coercion( + state: &mut CheckState, + context: &CheckContext, + left: TypeId, + right: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let left_kind = types::elaborate_kind(state, context, left)?; + let right_kind = types::elaborate_kind(state, context, right)?; + + let Some((left_applied, left_domain)) = + decompose_kind_for_coercion(state, context, left, left_kind)? + else { + return Ok(None); + }; + + let Some((right_applied, right_domain)) = + decompose_kind_for_coercion(state, context, right, right_kind)? + else { + return Ok(None); + }; + + if can_unify(state, context, left_domain, right_domain)? == CanUnify::Apart { + return Ok(Some(MatchInstance::Apart)); + } + + let argument = state.fresh_rigid(context.queries, left_domain); + let left_saturated = context.intern_application(left_applied, argument); + let right_saturated = context.intern_application(right_applied, argument); + let constraint = make_coercible_constraint(context, left_saturated, right_saturated); + + Ok(Some(MatchInstance::Match { constraints: vec![constraint], equalities: vec![] })) +} + +fn decompose_kind_for_coercion( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, + mut kind_id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + safe_loop! { + kind_id = normalise::normalise(state, context, kind_id)?; + match context.lookup_type(kind_id) { + Type::Forall(binder_id, inner_kind) => { + let binder = context.lookup_forall_binder(binder_id); + let fresh = state.fresh_rigid(context.queries, binder.kind); + type_id = context.intern_kind_application(type_id, fresh); + kind_id = SubstituteName::one(state, context, binder.name, fresh, inner_kind)?; + } + Type::Function(domain, _) => return Ok(Some((type_id, domain))), + _ => return Ok(None), + } + } +} + +fn try_refl( + state: &mut CheckState, + context: &CheckContext, + t1: TypeId, + t2: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let t1 = normalise::normalise(state, context, t1)?; + let t2 = normalise::normalise(state, context, t2)?; + + if t1 == t2 { + return Ok(true); + } + + match (context.lookup_type(t1), context.lookup_type(t2)) { + (Type::Application(f1, a1), Type::Application(f2, a2)) + | (Type::Constrained(f1, a1), Type::Constrained(f2, a2)) + | (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) + | (Type::Kinded(f1, a1), Type::Kinded(f2, a2)) => { + Ok(try_refl(state, context, f1, f2)? && try_refl(state, context, a1, a2)?) + } + + (Type::Function(a1, r1), Type::Function(a2, r2)) => { + Ok(try_refl(state, context, a1, a2)? && try_refl(state, context, r1, r2)?) + } + + (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { + let b1 = context.lookup_forall_binder(b1); + let b2 = context.lookup_forall_binder(b2); + Ok(try_refl(state, context, b1.kind, b2.kind)? && try_refl(state, context, i1, i2)?) + } + + (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { + Ok((f1, i1) == (f2, i2) + && try_refl(state, context, l1, l2)? + && try_refl(state, context, r1, r2)?) + } + + (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { + let s1 = context.lookup_synonym(s1); + let s2 = context.lookup_synonym(s2); + if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { + return Ok(false); + } + let s1_args = Arc::clone(&s1.arguments); + let s2_args = Arc::clone(&s2.arguments); + for (&a1, &a2) in s1_args.iter().zip(s2_args.iter()) { + if !try_refl(state, context, a1, a2)? { + return Ok(false); + } + } + Ok(true) + } + + (Type::Row(r1), Type::Row(r2)) => { + let r1 = context.lookup_row_type(r1); + let r2 = context.lookup_row_type(r2); + if r1.fields.len() != r2.fields.len() { + return Ok(false); + } + for (f1, f2) in r1.fields.iter().zip(r2.fields.iter()) { + if f1.label != f2.label || !try_refl(state, context, f1.id, f2.id)? { + return Ok(false); + } + } + match (r1.tail, r2.tail) { + (Some(t1), Some(t2)) => try_refl(state, context, t1, t2), + (None, None) => Ok(true), + _ => Ok(false), + } + } + + (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { + Ok(n1 == n2 && try_refl(state, context, k1, k2)?) + } + + _ => Ok(false), + } +} + +fn make_coercible_constraint(context: &CheckContext, left: TypeId, right: TypeId) -> TypeId +where + Q: ExternalQueries, +{ + let prim_coerce = &context.prim_coerce; + let coercible = Type::Constructor(prim_coerce.file_id, prim_coerce.coercible); + let coercible = context.queries.intern_type(coercible); + let coercible = context.intern_application(coercible, left); + context.intern_application(coercible, right) +} diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index ce46c968..78f1cc00 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -476,3 +476,95 @@ where }; Ok(matches!(type_item, Some(lowering::TypeItemIr::NewtypeGroup { .. }))) } + +pub fn extract_type_constructor( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Constructor(file_id, item_id) => return Ok(Some((file_id, item_id))), + Type::Application(function, _) | Type::KindApplication(function, _) => { + id = function; + } + _ => return Ok(None), + } + } +} + +pub fn is_constructor_in_scope( + context: &CheckContext, + file_id: FileId, + item_id: TypeItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let constructor_term_id = if file_id == context.id { + context.indexed.pairs.data_constructors(item_id).next() + } else { + let indexed = context.queries.indexed(file_id)?; + indexed.pairs.data_constructors(item_id).next() + }; + + let Some(constructor_term_id) = constructor_term_id else { + return Ok(false); + }; + + Ok(context.resolved.is_term_in_scope(&context.prim_resolved, file_id, constructor_term_id)) +} + +pub fn get_newtype_inner( + state: &mut CheckState, + context: &CheckContext, + newtype_file: FileId, + newtype_id: TypeItemId, + newtype_type: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let constructor_term_id = if newtype_file == context.id { + context.indexed.pairs.data_constructors(newtype_id).next() + } else { + let indexed = context.queries.indexed(newtype_file)?; + indexed.pairs.data_constructors(newtype_id).next() + }; + + let Some(constructor_term_id) = constructor_term_id else { + return Ok(None); + }; + + let constructor_type = lookup_file_term(state, context, newtype_file, constructor_term_id)?; + + let (_, arguments) = extract_type_application(state, context, newtype_type)?; + + let mut current = constructor_type; + let mut arguments = arguments.iter().copied(); + + safe_loop! { + current = normalise::normalise(state, context, current)?; + let Type::Forall(binder_id, inner) = context.lookup_type(current) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let replacement = arguments.next().unwrap_or_else(|| { + state.fresh_rigid(context.queries, binder.kind) + }); + + current = SubstituteName::one(state, context, binder.name, replacement, inner)?; + } + + current = normalise::normalise(state, context, current)?; + + let InspectFunction { arguments, .. } = inspect_function(state, context, current)?; + let [argument] = arguments[..] else { return Ok(None) }; + + Ok(Some(argument)) +} From 10e0f8746d3b5a5402d506b27a139f5a4ad82ce9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 01:31:26 +0800 Subject: [PATCH 298/386] Add tests for Prim.Coerce --- .../068_prim_coercible_reflexivity/Main.purs | 12 ++++++++ .../068_prim_coercible_reflexivity/Main.snap | 11 ++++++++ .../069_prim_coercible_newtype/Main.purs | 19 +++++++++++++ .../069_prim_coercible_newtype/Main.snap | 20 +++++++++++++ .../070_prim_coercible_roles/Main.purs | 28 +++++++++++++++++++ .../070_prim_coercible_roles/Main.snap | 25 +++++++++++++++++ .../071_prim_coercible_apart/Main.purs | 10 +++++++ .../071_prim_coercible_apart/Main.snap | 27 ++++++++++++++++++ .../Lib.purs | 3 ++ .../Main.purs | 7 +++++ .../Main.snap | 28 +++++++++++++++++++ .../073_prim_coercible_higher_kinded/Lib.purs | 8 ++++++ .../Main.purs | 10 +++++++ .../Main.snap | 10 +++++++ .../074_prim_coercible_transitivity/Main.purs | 12 ++++++++ .../074_prim_coercible_transitivity/Main.snap | 18 ++++++++++++ .../tests/checking2/generated.rs | 14 ++++++++++ 17 files changed, 262 insertions(+) create mode 100644 tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.purs create mode 100644 tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.snap create mode 100644 tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.purs create mode 100644 tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.snap create mode 100644 tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.purs create mode 100644 tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.snap create mode 100644 tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.purs create mode 100644 tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.snap create mode 100644 tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Lib.purs create mode 100644 tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.snap create mode 100644 tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Lib.purs create mode 100644 tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.purs create mode 100644 tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.snap diff --git a/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.purs b/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.purs new file mode 100644 index 00000000..ff1b6009 --- /dev/null +++ b/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Safe.Coerce (coerce) + +testInt :: Int -> Int +testInt = coerce + +testString :: String -> String +testString = coerce + +testPoly :: forall a. a -> a +testPoly = coerce diff --git a/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.snap b/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.snap new file mode 100644 index 00000000..80215717 --- /dev/null +++ b/tests-integration/fixtures/checking2/068_prim_coercible_reflexivity/Main.snap @@ -0,0 +1,11 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testInt :: Int -> Int +testString :: String -> String +testPoly :: forall (a :: Type). (a :: Type) -> (a :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.purs b/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.purs new file mode 100644 index 00000000..875e3ce9 --- /dev/null +++ b/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.purs @@ -0,0 +1,19 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int + +wrapAge :: Int -> Age +wrapAge = coerce + +unwrapAge :: Age -> Int +unwrapAge = coerce + +newtype Wrapper a = Wrapper a + +wrapValue :: forall a. a -> Wrapper a +wrapValue = coerce + +unwrapValue :: forall a. Wrapper a -> a +unwrapValue = coerce diff --git a/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.snap b/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.snap new file mode 100644 index 00000000..9348fb03 --- /dev/null +++ b/tests-integration/fixtures/checking2/069_prim_coercible_newtype/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +wrapAge :: Int -> Age +unwrapAge :: Age -> Int +Wrapper :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) +wrapValue :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) +unwrapValue :: forall (a :: Type). Wrapper (a :: Type) -> (a :: Type) + +Types +Age :: Type +Wrapper :: Type -> Type + +Roles +Age = [] +Wrapper = [Representational] diff --git a/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.purs b/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.purs new file mode 100644 index 00000000..d1c2810a --- /dev/null +++ b/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.purs @@ -0,0 +1,28 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int + +-- Phantom: different inner types are fine +data Proxy a = Proxy + +coerceProxy :: Proxy Int -> Proxy String +coerceProxy = coerce + +-- Representational: Coercible inner propagates +data Maybe a = Nothing | Just a + +coerceMaybe :: Maybe Age -> Maybe Int +coerceMaybe = coerce + +coerceMaybeReverse :: Maybe Int -> Maybe Age +coerceMaybeReverse = coerce + +-- Record coercion (row decomposition) +coerceRecord :: { name :: String, age :: Age } -> { name :: String, age :: Int } +coerceRecord = coerce + +-- Function decomposition +coerceFunction :: (Int -> Age) -> (Int -> Int) +coerceFunction = coerce diff --git a/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.snap b/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.snap new file mode 100644 index 00000000..db78772e --- /dev/null +++ b/tests-integration/fixtures/checking2/070_prim_coercible_roles/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 :: Type)) +coerceProxy :: Proxy @Type Int -> Proxy @Type String +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +coerceMaybe :: Maybe Age -> Maybe Int +coerceMaybeReverse :: Maybe Int -> Maybe Age +coerceRecord :: { age :: Age, name :: String } -> { age :: Int, name :: String } +coerceFunction :: (Int -> Age) -> Int -> Int + +Types +Age :: Type +Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type +Maybe :: Type -> Type + +Roles +Age = [] +Proxy = [Phantom] +Maybe = [Representational] diff --git a/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.purs b/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.purs new file mode 100644 index 00000000..3fcb7b8b --- /dev/null +++ b/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Safe.Coerce (coerce) + +data Maybe a = Nothing | Just a +data Either a b = Left a | Right b + +-- Different type constructors: should fail +coerceDifferent :: Maybe Int -> Either Int String +coerceDifferent = coerce diff --git a/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.snap b/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.snap new file mode 100644 index 00000000..c576118c --- /dev/null +++ b/tests-integration/fixtures/checking2/071_prim_coercible_apart/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +coerceDifferent :: Maybe Int -> Either Int String + +Types +Maybe :: Type -> Type +Either :: Type -> Type -> Type + +Roles +Maybe = [Representational] +Either = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Lib.purs b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Lib.purs new file mode 100644 index 00000000..4eb11d87 --- /dev/null +++ b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Lib.purs @@ -0,0 +1,3 @@ +module Lib (HiddenAge) where + +newtype HiddenAge = HiddenAge Int diff --git a/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.purs b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.purs new file mode 100644 index 00000000..189a3fc5 --- /dev/null +++ b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Lib (HiddenAge) +import Safe.Coerce (coerce) + +coerceHidden :: Int -> HiddenAge +coerceHidden = coerce diff --git a/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.snap b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.snap new file mode 100644 index 00000000..aa6044f7 --- /dev/null +++ b/tests-integration/fixtures/checking2/072_prim_coercible_hidden_constructor/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceHidden :: Int -> HiddenAge + +Types + +Errors +CheckError { + kind: CoercibleConstructorNotInScope { + file_id: Idx::(39), + item_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Lib.purs b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Lib.purs new file mode 100644 index 00000000..2cfa34fa --- /dev/null +++ b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Lib.purs @@ -0,0 +1,8 @@ +module Lib where + +data Maybe a = Nothing | Just a + +newtype MaybeAlias a = MaybeAlias (Maybe a) + +foreign import data Container :: (Type -> Type) -> Type +type role Container representational diff --git a/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.purs new file mode 100644 index 00000000..53b9f8ae --- /dev/null +++ b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Safe.Coerce (coerce) +import Lib (Maybe(..), MaybeAlias(..), Container) + +coerceContainer :: Container Maybe -> Container MaybeAlias +coerceContainer = coerce + +coerceContainerReverse :: Container MaybeAlias -> Container Maybe +coerceContainerReverse = coerce diff --git a/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.snap new file mode 100644 index 00000000..1ba43f11 --- /dev/null +++ b/tests-integration/fixtures/checking2/073_prim_coercible_higher_kinded/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceContainer :: Container Maybe -> Container MaybeAlias +coerceContainerReverse :: Container MaybeAlias -> Container Maybe + +Types diff --git a/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.purs b/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.purs new file mode 100644 index 00000000..f871ac88 --- /dev/null +++ b/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int +newtype Years = Years Age + +coerceTransitive :: Int -> Years +coerceTransitive = coerce + +unwrapTransitive :: Years -> Int +unwrapTransitive = coerce diff --git a/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.snap b/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.snap new file mode 100644 index 00000000..6897aabc --- /dev/null +++ b/tests-integration/fixtures/checking2/074_prim_coercible_transitivity/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +Years :: Age -> Years +coerceTransitive :: Int -> Years +unwrapTransitive :: Years -> Int + +Types +Age :: Type +Years :: Type + +Roles +Age = [] +Years = [] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 34203718..388108f5 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -163,3 +163,17 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_066_compiler_solved_superclass_given_main() { run_test("066_compiler_solved_superclass_given", "Main"); } #[rustfmt::skip] #[test] fn test_067_compiler_solved_superclass_minimisation_main() { run_test("067_compiler_solved_superclass_minimisation", "Main"); } + +#[rustfmt::skip] #[test] fn test_068_prim_coercible_reflexivity_main() { run_test("068_prim_coercible_reflexivity", "Main"); } + +#[rustfmt::skip] #[test] fn test_069_prim_coercible_newtype_main() { run_test("069_prim_coercible_newtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_070_prim_coercible_roles_main() { run_test("070_prim_coercible_roles", "Main"); } + +#[rustfmt::skip] #[test] fn test_071_prim_coercible_apart_main() { run_test("071_prim_coercible_apart", "Main"); } + +#[rustfmt::skip] #[test] fn test_072_prim_coercible_hidden_constructor_main() { run_test("072_prim_coercible_hidden_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_073_prim_coercible_higher_kinded_main() { run_test("073_prim_coercible_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_074_prim_coercible_transitivity_main() { run_test("074_prim_coercible_transitivity", "Main"); } From f527d5bd9a101b3963071f8c4a034a52338b1328 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 02:30:45 +0800 Subject: [PATCH 299/386] Elaborate coercible into symmetric --- .../checking2/src/core/constraint/given.rs | 25 ++++++++++++++++--- .../Main.purs | 9 +++++++ .../Main.snap | 14 +++++++++++ .../tests/checking2/generated.rs | 2 ++ 4 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.purs create mode 100644 tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.snap diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index ebf2691c..7f34e0f9 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -10,7 +10,6 @@ use super::{ ConstraintApplication, MatchInstance, MatchType, constraint_application, elaborate_superclasses, }; -/// Discovers implied constraints from given constraints. pub fn elaborate_given( state: &mut CheckState, context: &CheckContext, @@ -26,11 +25,31 @@ where elaborate_superclasses(state, context, constraint, &mut elaborated)?; } - elaborated + let mut applications = elaborated .into_iter() .map(|constraint| constraint_application(state, context, constraint)) .filter_map_ok(|constraint| constraint) - .collect() + .collect::>>()?; + + let symmetric = applications.iter().filter_map(|application| { + let is_coercible = application.file_id == context.prim_coerce.file_id + && application.item_id == context.prim_coerce.coercible; + + let &[left, right] = application.arguments.as_slice() else { + return None; + }; + + is_coercible.then(|| ConstraintApplication { + file_id: application.file_id, + item_id: application.item_id, + arguments: vec![right, left], + }) + }); + + let symmetric = symmetric.collect_vec(); + applications.extend(symmetric); + + Ok(applications) } /// Matches a wanted constraint to given constraints. diff --git a/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.purs b/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.purs new file mode 100644 index 00000000..409937d2 --- /dev/null +++ b/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Safe.Coerce (class Coercible, coerce) + +test :: forall a b. Coercible b a => a -> b +test = coerce + +test' :: forall a b. Coercible b a => a -> b +test' value = coerce value diff --git a/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.snap b/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.snap new file mode 100644 index 00000000..aca2487c --- /dev/null +++ b/tests-integration/fixtures/checking2/075_prim_coercible_given_symmetry/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: + forall (a :: Type) (b :: Type). + Coercible @Type (b :: Type) (a :: Type) => (a :: Type) -> (b :: Type) +test' :: + forall (a :: Type) (b :: Type). + Coercible @Type (b :: Type) (a :: Type) => (a :: Type) -> (b :: Type) + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 388108f5..8ec6ab85 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -177,3 +177,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_073_prim_coercible_higher_kinded_main() { run_test("073_prim_coercible_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_074_prim_coercible_transitivity_main() { run_test("074_prim_coercible_transitivity", "Main"); } + +#[rustfmt::skip] #[test] fn test_075_prim_coercible_given_symmetry_main() { run_test("075_prim_coercible_given_symmetry", "Main"); } From 06db6680ac41bbc433a4b838f0d5bad8d28b9849 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 04:26:18 +0800 Subject: [PATCH 300/386] Implement instance member checking --- .../src/core/constraint/instances.rs | 50 +--- compiler-core/checking2/src/core/toolkit.rs | 47 +++- compiler-core/checking2/src/error.rs | 2 +- .../checking2/src/source/term_items.rs | 231 ++++++++++++++++-- .../checking2/src/source/terms/equations.rs | 43 +++- .../checking2/src/source/terms/form_let.rs | 2 +- 6 files changed, 302 insertions(+), 73 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index dc46c25f..2bc1dab1 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -10,13 +10,13 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::unification::{CanUnify, can_unify}; use crate::core::walk::{self, TypeWalker, WalkAction}; -use crate::core::{CheckedInstance, ForallBinder, Name, Type, TypeId, normalise, toolkit}; +use crate::core::{CheckedInstance, Name, Type, TypeId, normalise, toolkit}; use crate::error::ErrorKind; use crate::state::CheckState; -use crate::{CheckedModule, ExternalQueries, safe_loop}; +use crate::{CheckedModule, ExternalQueries}; use super::fd::{Fd, compute_closure, get_all_determined}; -use super::{ConstraintApplication, MatchInstance, MatchType, constraint_application}; +use super::{ConstraintApplication, MatchInstance, MatchType}; #[derive(Clone)] pub struct CandidateInstance { @@ -25,12 +25,6 @@ pub struct CandidateInstance { checked: CheckedInstance, } -struct DecomposedInstance { - binders: Vec, - arguments: Vec, - constraints: Vec, -} - /// Collects instance chains for a constraint from all eligible files. pub fn collect_instance_chains( state: &mut CheckState, @@ -170,42 +164,6 @@ where Ok(stuck_positions.iter().all(|index| determined.contains(index))) } -fn decompose_instance( - state: &mut CheckState, - context: &CheckContext, - instance: &CheckedInstance, -) -> QueryResult> -where - Q: ExternalQueries, -{ - let toolkit::InspectQuantified { binders, quantified } = - toolkit::inspect_quantified(state, context, instance.canonical)?; - - let mut current = quantified; - let mut constraints = vec![]; - - safe_loop! { - current = normalise::normalise(state, context, current)?; - match context.lookup_type(current) { - Type::Constrained(constraint, constrained) => { - constraints.push(constraint); - current = constrained; - } - _ => break, - } - } - - let Some(application) = constraint_application(state, context, current)? else { - return Ok(None); - }; - - if (application.file_id, application.item_id) != instance.resolution { - return Ok(None); - } - - Ok(Some(DecomposedInstance { binders, arguments: application.arguments, constraints })) -} - /// Matches a wanted constraint to an instance. pub fn match_instance( state: &mut CheckState, @@ -216,7 +174,7 @@ pub fn match_instance( where Q: ExternalQueries, { - let Some(decomposed) = decompose_instance(state, context, &instance.checked)? else { + let Some(decomposed) = toolkit::decompose_instance(state, context, &instance.checked)? else { return Ok(MatchInstance::Apart); }; diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 78f1cc00..8da80b11 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -9,7 +9,8 @@ use indexing::{TermItemId, TypeItemId}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, normalise, unification, + CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, Role, Type, TypeId, constraint, + normalise, unification, }; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -24,6 +25,12 @@ pub struct InspectFunction { pub result: TypeId, } +pub struct DecomposedInstance { + pub binders: Vec, + pub constraints: Vec, + pub arguments: Vec, +} + pub fn extract_type_application( state: &mut CheckState, context: &CheckContext, @@ -266,6 +273,44 @@ where Ok(InspectFunction { arguments, result: current }) } +pub fn decompose_instance( + state: &mut CheckState, + context: &CheckContext, + instance: &CheckedInstance, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let InspectQuantified { binders, quantified } = + inspect_quantified(state, context, instance.canonical)?; + + let mut current = quantified; + let mut constraints = vec![]; + + safe_loop! { + current = normalise::normalise(state, context, current)?; + match context.lookup_type(current) { + Type::Constrained(constraint, constrained) => { + constraints.push(constraint); + current = constrained; + } + _ => break, + } + } + + let Some(constraint::ConstraintApplication { file_id, item_id, arguments }) = + constraint::constraint_application(state, context, current)? + else { + return Ok(None); + }; + + if (file_id, item_id) != instance.resolution { + return Ok(None); + } + + Ok(Some(DecomposedInstance { binders, constraints, arguments })) +} + pub fn instantiate_unifications( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/error.rs b/compiler-core/checking2/src/error.rs index 5eef0b1f..855a405c 100644 --- a/compiler-core/checking2/src/error.rs +++ b/compiler-core/checking2/src/error.rs @@ -104,7 +104,7 @@ pub enum ErrorKind { item_id: indexing::TypeItemId, }, TooManyBinders { - signature: lowering::TypeId, + signature: Option, expected: u32, actual: u32, }, diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 6e4f6740..eb80e3a2 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -8,8 +8,9 @@ use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::context::CheckContext; +use crate::core::substitute::{NameToType, SubstituteName}; use crate::core::{ - CheckedInstance, Type, TypeId, constraint, generalise, toolkit, unification, zonk, + CheckedInstance, Type, TypeId, constraint, generalise, normalise, toolkit, unification, zonk, }; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::terms::equations; @@ -33,6 +34,7 @@ where { check_instance_declarations(state, context)?; check_value_groups(state, context)?; + check_instance_members(state, context)?; Ok(()) } @@ -174,6 +176,211 @@ where Ok(()) } +fn check_instance_members(state: &mut CheckState, context: &CheckContext) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for scc in &context.grouped.term_scc { + for &item_id in scc.as_slice() { + let Some(TermItemIr::Instance { members, resolution, .. }) = + context.lowered.info.get_term_item(item_id) + else { + continue; + }; + + let Some((class_file, class_id)) = *resolution else { + continue; + }; + + let TermItemKind::Instance { id: instance_id } = context.indexed.items[item_id].kind + else { + continue; + }; + + let Some(instance) = state.checked.lookup_instance(instance_id) else { + continue; + }; + + let Some(instance) = toolkit::decompose_instance(state, context, &instance)? else { + continue; + }; + + for member in members.iter() { + check_instance_member_group( + state, + context, + item_id, + member, + (class_file, class_id), + &instance.constraints, + &instance.arguments, + )?; + } + } + } + + Ok(()) +} + +fn check_instance_member_group( + state: &mut CheckState, + context: &CheckContext, + instance_item_id: TermItemId, + member: &lowering::InstanceMemberGroup, + (class_file, class_id): (FileId, TypeItemId), + instance_constraints: &[TypeId], + instance_arguments: &[TypeId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + state.with_error_crumb(ErrorCrumb::TermDeclaration(instance_item_id), |state| { + state.with_implication(|state| { + check_instance_member_group_core( + state, + context, + member, + (class_file, class_id), + instance_constraints, + instance_arguments, + ) + }) + }) +} + +fn check_instance_member_group_core( + state: &mut CheckState, + context: &CheckContext, + member: &lowering::InstanceMemberGroup, + (class_file, class_id): (FileId, TypeItemId), + instance_constraints: &[TypeId], + instance_arguments: &[TypeId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for &constraint in instance_constraints { + state.push_given(constraint); + } + + let class_member_type = if let Some((member_file, member_id)) = member.resolution { + Some(toolkit::lookup_file_term(state, context, member_file, member_id)?) + } else { + None + }; + + let class_member_type = if let Some(class_member_type) = class_member_type { + specialise_class_member_type( + state, + context, + class_member_type, + (class_file, class_id), + instance_arguments, + )? + } else { + None + }; + + let residuals = if let Some(signature_id) = member.signature { + let (signature_member_type, _) = + types::check_kind(state, context, signature_id, context.prim.t)?; + + if let Some(class_member_type) = class_member_type { + let unified = + unification::unify(state, context, signature_member_type, class_member_type)?; + if !unified { + let expected = state.pretty_id(context, class_member_type)?; + let actual = state.pretty_id(context, signature_member_type)?; + state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); + } + } + + equations::check_equations_with( + state, + context, + equations::EquationTypeOrigin::Explicit(signature_id), + signature_member_type, + &member.equations, + )? + } else if let Some(specialised_type) = class_member_type { + equations::check_equations_with( + state, + context, + equations::EquationTypeOrigin::Implicit, + specialised_type, + &member.equations, + )? + } else { + vec![] + }; + + for residual in residuals { + let constraint = state.pretty_id(context, residual)?; + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + + Ok(()) +} + +fn specialise_class_member_type( + state: &mut CheckState, + context: &CheckContext, + class_member_type: TypeId, + (class_file, class_id): (FileId, TypeItemId), + instance_arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(class_info) = toolkit::lookup_file_class(state, context, class_file, class_id)? else { + return Ok(None); + }; + + let toolkit::InspectQuantified { binders, quantified } = + toolkit::inspect_quantified(state, context, class_member_type)?; + + let class_binder_count = class_info.kind_binders.len() + class_info.type_parameters.len(); + if binders.len() < class_binder_count + || instance_arguments.len() != class_info.type_parameters.len() + { + return Ok(None); + } + + let (class_binders, member_binders) = binders.split_at(class_binder_count); + let (kind_binders, type_binders) = class_binders.split_at(class_info.kind_binders.len()); + + let mut bindings = NameToType::default(); + for binder in kind_binders { + let replacement = state.fresh_unification(context.queries, binder.kind); + bindings.insert(binder.name, replacement); + } + for (binder, &argument) in type_binders.iter().zip(instance_arguments) { + bindings.insert(binder.name, argument); + } + + let mut specialised = SubstituteName::many(state, context, &bindings, quantified)?; + specialised = normalise::normalise(state, context, specialised)?; + + let Type::Constrained(constraint, mut constrained) = context.lookup_type(specialised) else { + return Ok(None); + }; + + let Some(application) = constraint::constraint_application(state, context, constraint)? else { + return Ok(None); + }; + + if (application.file_id, application.item_id) != (class_file, class_id) { + return Ok(None); + } + + for binder in member_binders.iter().rev() { + let binder_id = context.intern_forall_binder(*binder); + constrained = context.intern_forall(binder_id, constrained); + } + + Ok(Some(constrained)) +} + fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TermItemId]) where Q: ExternalQueries, @@ -306,27 +513,13 @@ fn check_value_group_core_check( where Q: ExternalQueries, { - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, signature_type)?; - - let quantified = toolkit::collect_givens(state, context, quantified)?; - - let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function(state, context, quantified)?; - - let function = context.intern_function_chain(&arguments, result); - - equations::check_equations_core( + equations::check_equations_with( state, context, - signature_id, - &arguments, - result, - function, + equations::EquationTypeOrigin::Explicit(signature_id), + signature_type, equations, - )?; - - state.solve_constraints(context) + ) } fn check_value_group_core_infer( diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index b407e950..6801f1f7 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -10,6 +10,39 @@ use crate::source::terms::form_let; use crate::source::{binder, terms}; use crate::state::CheckState; +pub enum EquationTypeOrigin { + Explicit(lowering::TypeId), + Implicit, +} + +pub fn check_equations_with( + state: &mut CheckState, + context: &CheckContext, + origin: EquationTypeOrigin, + expected_type: TypeId, + equations: &[lowering::Equation], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, expected_type)?; + + let quantified = toolkit::collect_givens(state, context, quantified)?; + + let toolkit::InspectFunction { arguments, result } = + toolkit::inspect_function(state, context, quantified)?; + + let function = context.intern_function_chain(&arguments, result); + check_equations_core(state, context, origin, &arguments, result, function, equations)?; + + let exhaustiveness = + exhaustive::check_equation_patterns(state, context, &arguments, equations)?; + state.report_exhaustiveness(context, exhaustiveness); + + state.solve_constraints(context) +} + /// Infers the type of value group equations. /// /// For each equation: infer binders, create a result unification variable, @@ -64,7 +97,7 @@ where pub fn check_equations_core( state: &mut CheckState, context: &CheckContext, - signature_id: lowering::TypeId, + origin: EquationTypeOrigin, arguments: &[TypeId], result: TypeId, function: TypeId, @@ -80,7 +113,10 @@ where if equation_arity > expected_arity { state.insert_error(ErrorKind::TooManyBinders { - signature: signature_id, + signature: match origin { + EquationTypeOrigin::Explicit(signature_id) => Some(signature_id), + EquationTypeOrigin::Implicit => None, + }, expected: expected_arity as u32, actual: equation_arity as u32, }); @@ -110,9 +146,6 @@ where } } - let exhaustiveness = exhaustive::check_equation_patterns(state, context, arguments, equations)?; - state.report_exhaustiveness(context, exhaustiveness); - Ok(()) } diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 5bafdaa0..18a9b149 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -152,7 +152,7 @@ where equations::check_equations_core( state, context, - signature_id, + equations::EquationTypeOrigin::Explicit(signature_id), &arguments, result, function, From d8d08b4876e80bff3816acf4b83d82ef24dd9c72 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 2 Mar 2026 04:26:18 +0800 Subject: [PATCH 301/386] Add instance member checking tests --- .../076_instance_member_functor/Main.purs | 10 ++++ .../076_instance_member_functor/Main.snap | 32 ++++++++++++ .../Main.purs | 11 ++++ .../Main.snap | 22 ++++++++ .../Main.purs | 11 ++++ .../Main.snap | 34 ++++++++++++ .../Main.purs | 9 ++++ .../Main.snap | 52 +++++++++++++++++++ .../Main.purs | 11 ++++ .../Main.snap | 36 +++++++++++++ .../Main.purs | 8 +++ .../Main.snap | 31 +++++++++++ .../tests/checking2/generated.rs | 12 +++++ 13 files changed, 279 insertions(+) create mode 100644 tests-integration/fixtures/checking2/076_instance_member_functor/Main.purs create mode 100644 tests-integration/fixtures/checking2/076_instance_member_functor/Main.snap create mode 100644 tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.purs create mode 100644 tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.snap create mode 100644 tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.purs create mode 100644 tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.snap create mode 100644 tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.purs create mode 100644 tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.snap create mode 100644 tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.purs create mode 100644 tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.snap diff --git a/tests-integration/fixtures/checking2/076_instance_member_functor/Main.purs b/tests-integration/fixtures/checking2/076_instance_member_functor/Main.purs new file mode 100644 index 00000000..981ea1a5 --- /dev/null +++ b/tests-integration/fixtures/checking2/076_instance_member_functor/Main.purs @@ -0,0 +1,10 @@ +module Main where + +class Functor :: (Type -> Type) -> Constraint +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +data Box a = Box a + +instance Functor Box where + map f (Box x) = Box (f x) diff --git a/tests-integration/fixtures/checking2/076_instance_member_functor/Main.snap b/tests-integration/fixtures/checking2/076_instance_member_functor/Main.snap new file mode 100644 index 00000000..a7b2c5b7 --- /dev/null +++ b/tests-integration/fixtures/checking2/076_instance_member_functor/Main.snap @@ -0,0 +1,32 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) + +Types +Functor :: (Type -> Type) -> Constraint +Box :: Type -> Type + +Classes +class forall (f :: Type -> Type). Functor (f :: Type -> Type) + map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) + +Instances +instance Functor Box + +Roles +Box = [Representational] diff --git a/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.purs b/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.purs new file mode 100644 index 00000000..e79b3287 --- /dev/null +++ b/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Show :: Type -> Constraint +class Show a where + show :: a -> String + +data Box a = Box a + +instance Show a => Show (Box a) where + show :: Box a -> String + show (Box x) = show x diff --git a/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.snap b/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.snap new file mode 100644 index 00000000..f98d59ca --- /dev/null +++ b/tests-integration/fixtures/checking2/077_instance_member_signature_head_variable/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) + +Types +Show :: Type -> Constraint +Box :: Type -> Type + +Classes +class forall (a :: Type). Show (a :: Type) + show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Instances +instance forall (t2 :: Type). Show (t2 :: Type) => Show (Box (t2 :: Type)) + +Roles +Box = [Representational] diff --git a/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.purs b/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.purs new file mode 100644 index 00000000..1f206ccb --- /dev/null +++ b/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Show :: Type -> Constraint +class Show a where + show :: a -> String + +data Box a = Box a + +instance Show (Box a) where + show :: Box a -> String + show (Box x) = show x diff --git a/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.snap b/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.snap new file mode 100644 index 00000000..26f8e693 --- /dev/null +++ b/tests-integration/fixtures/checking2/078_instance_member_missing_constraint/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) + +Types +Show :: Type -> Constraint +Box :: Type -> Type + +Classes +class forall (a :: Type). Show (a :: Type) + show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Instances +instance forall (t2 :: Type). Show (Box (t2 :: Type)) + +Roles +Box = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.purs b/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.purs new file mode 100644 index 00000000..8a3a37b1 --- /dev/null +++ b/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.purs @@ -0,0 +1,9 @@ +module Main where + +class Show :: Type -> Constraint +class Show a where + show :: a -> String + +instance Show Int where + show :: Int -> Int + show x = x diff --git a/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.snap b/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.snap new file mode 100644 index 00000000..9a8ee83b --- /dev/null +++ b/tests-integration/fixtures/checking2/079_instance_member_signature_mismatch/Main.snap @@ -0,0 +1,52 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Types +Show :: Type -> Constraint + +Classes +class forall (a :: Type). Show (a :: Type) + show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Instances +instance Show Int + +Errors +CheckError { + kind: CannotUnify { + t1: Id(3), + t2: Id(4), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: InstanceMemberTypeMismatch { + expected: Id(6), + actual: Id(5), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.purs new file mode 100644 index 00000000..60166d66 --- /dev/null +++ b/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Functor :: (Type -> Type) -> Constraint +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +newtype Wrap :: forall k. Type -> (k -> Type) -> k -> Type +newtype Wrap e w a = Wrap (w a) + +instance Functor w => Functor (Wrap e w) where + map f (Wrap x) = Wrap (map f x) diff --git a/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.snap new file mode 100644 index 00000000..b2f697dd --- /dev/null +++ b/tests-integration/fixtures/checking2/080_instance_member_higher_kinded/Main.snap @@ -0,0 +1,36 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +Wrap :: + forall (k :: Type) (e :: Type) (w :: (k :: Type) -> Type) (a :: (k :: Type)). + (w :: (k :: Type) -> Type) (a :: (k :: Type)) -> + Wrap @(k :: Type) (e :: Type) (w :: (k :: Type) -> Type) (a :: (k :: Type)) + +Types +Functor :: (Type -> Type) -> Constraint +Wrap :: forall (k :: Type). Type -> ((k :: Type) -> Type) -> (k :: Type) -> Type + +Classes +class forall (f :: Type -> Type). Functor (f :: Type -> Type) + map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) + +Instances +instance forall (t8 :: Type -> Type) (t7 :: Type). + Functor (t8 :: Type -> Type) => Functor (Wrap @Type (t7 :: Type) (t8 :: Type -> Type)) + +Roles +Wrap = [Phantom, Representational, Nominal] diff --git a/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.purs b/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.purs new file mode 100644 index 00000000..e2c9c5b3 --- /dev/null +++ b/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.purs @@ -0,0 +1,8 @@ +module Main where + +class Show :: Type -> Constraint +class Show a where + show :: a -> String + +instance Show Int where + show x y = show x diff --git a/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.snap b/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.snap new file mode 100644 index 00000000..b58c3bb6 --- /dev/null +++ b/tests-integration/fixtures/checking2/081_instance_member_too_many_binders/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Types +Show :: Type -> Constraint + +Classes +class forall (a :: Type). Show (a :: Type) + show :: forall (a :: Type). Show (a :: Type) => (a :: Type) -> String + +Instances +instance Show Int + +Errors +CheckError { + kind: TooManyBinders { + signature: None, + expected: 1, + actual: 2, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 8ec6ab85..92dd48eb 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -179,3 +179,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_074_prim_coercible_transitivity_main() { run_test("074_prim_coercible_transitivity", "Main"); } #[rustfmt::skip] #[test] fn test_075_prim_coercible_given_symmetry_main() { run_test("075_prim_coercible_given_symmetry", "Main"); } + +#[rustfmt::skip] #[test] fn test_076_instance_member_functor_main() { run_test("076_instance_member_functor", "Main"); } + +#[rustfmt::skip] #[test] fn test_077_instance_member_signature_head_variable_main() { run_test("077_instance_member_signature_head_variable", "Main"); } + +#[rustfmt::skip] #[test] fn test_078_instance_member_missing_constraint_main() { run_test("078_instance_member_missing_constraint", "Main"); } + +#[rustfmt::skip] #[test] fn test_079_instance_member_signature_mismatch_main() { run_test("079_instance_member_signature_mismatch", "Main"); } + +#[rustfmt::skip] #[test] fn test_080_instance_member_higher_kinded_main() { run_test("080_instance_member_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_081_instance_member_too_many_binders_main() { run_test("081_instance_member_too_many_binders", "Main"); } From 98db3f9236ecbe495a961ea6144099f8bbdee4d8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 02:14:49 +0800 Subject: [PATCH 302/386] Use imports where possible --- .../core/constraint/compiler/prim_row_list.rs | 4 ++-- .../src/core/constraint/instances.rs | 19 +++++++++---------- .../checking2/src/core/generalise.rs | 8 ++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs index 9dec2fa6..fbe5b6a2 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs @@ -5,7 +5,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::constraint::MatchInstance; -use crate::core::{RowField, RowType, TypeId, normalise}; +use crate::core::{RowField, RowType, Type, TypeId, normalise}; use crate::source::types; use crate::state::CheckState; @@ -67,7 +67,7 @@ where let row_kind = types::elaborate_kind(state, context, singleton_row_type)?; let row_kind = normalise::normalise(state, context, row_kind)?; - let crate::core::Type::Application(_, element_kind) = context.lookup_type(row_kind) else { + let Type::Application(_, element_kind) = context.lookup_type(row_kind) else { return Ok(state.fresh_unification(context.queries, context.prim.t)); }; diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index 2bc1dab1..b0e23532 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -9,8 +9,8 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::unification::{CanUnify, can_unify}; -use crate::core::walk::{self, TypeWalker, WalkAction}; -use crate::core::{CheckedInstance, Name, Type, TypeId, normalise, toolkit}; +use crate::core::walk::{TypeWalker, WalkAction, walk_type}; +use crate::core::{CheckedInstance, Name, RowField, RowType, Type, TypeId, normalise, toolkit}; use crate::error::ErrorKind; use crate::state::CheckState; use crate::{CheckedModule, ExternalQueries}; @@ -372,8 +372,8 @@ fn match_row_type( context: &CheckContext, bindings: &mut FxHashMap, equalities: &mut Vec<(TypeId, TypeId)>, - wanted_row: crate::core::RowType, - given_row: crate::core::RowType, + wanted_row: RowType, + given_row: RowType, ) -> QueryResult where Q: ExternalQueries, @@ -413,7 +413,7 @@ where } impl RowRest { - fn new(only: &[&crate::core::RowField], tail: Option) -> RowRest { + fn new(only: &[&RowField], tail: Option) -> RowRest { if !only.is_empty() { RowRest::Additional } else if let Some(tail) = tail { @@ -443,9 +443,8 @@ where // additional fields and tail from the wanted row Open(given_tail) => { let fields = wanted_only.into_iter().cloned().collect_vec(); - let row = context.intern_row( - context.intern_row_type(crate::core::RowType::new(fields, wanted_row.tail)), - ); + let row = + context.intern_row(context.intern_row_type(RowType::new(fields, wanted_row.tail))); result.and_then(|| match_type(state, context, bindings, equalities, row, given_tail)) } // If we have a closed given row @@ -512,7 +511,7 @@ impl<'a> CollectFileReferences<'a> { where Q: ExternalQueries, { - walk::walk_type(state, context, id, &mut CollectFileReferences { files }) + walk_type(state, context, id, &mut CollectFileReferences { files }) } } @@ -548,7 +547,7 @@ impl HasLabeledRole { Q: ExternalQueries, { let mut walker = HasLabeledRole { contains: false }; - walk::walk_type(state, context, id, &mut walker)?; + walk_type(state, context, id, &mut walker)?; Ok(walker.contains) } } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index a373260e..34329f2f 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -25,7 +25,7 @@ use petgraph::prelude::DiGraphMap; use rustc_hash::FxHashSet; use crate::context::CheckContext; -use crate::core::walk::{self, TypeWalker, WalkAction}; +use crate::core::walk::{TypeWalker, WalkAction, walk_type}; use crate::core::{ForallBinder, Name, Type, TypeId, constraint, normalise, zonk}; use crate::state::{CheckState, UnificationEntry, UnificationState}; use crate::{ExternalQueries, safe_loop}; @@ -415,7 +415,7 @@ impl TypeWalker for GeneraliseImplicit { self.graph.add_edge(prev_owner, next_owner, ()); } - walk::walk_type(state, context, *kind, self)?; + walk_type(state, context, *kind, self)?; self.owner = prev_owner; } Type::Unification(id) => { @@ -429,7 +429,7 @@ impl TypeWalker for GeneraliseImplicit { self.graph.add_edge(prev_owner, next_owner, ()); } - walk::walk_type(state, context, *kind, self)?; + walk_type(state, context, *kind, self)?; self.owner = prev_owner; } _ => {} @@ -451,7 +451,7 @@ where Q: ExternalQueries, { let mut walker = GeneraliseImplicit::default(); - walk::walk_type(state, context, id, &mut walker)?; + walk_type(state, context, id, &mut walker)?; let Ok(implicits_unifications) = algo::toposort(&walker.graph, None) else { return Ok(context.unknown("invalid recursive graph")); From 5d2077813577eb722d8b34e079dba83ccbeedb8e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 02:45:00 +0800 Subject: [PATCH 303/386] Implement scaffolding for derive instance port --- .../src/core/constraint/instances.rs | 9 + compiler-core/checking2/src/error.rs | 4 + compiler-core/checking2/src/lib.rs | 7 +- compiler-core/checking2/src/source.rs | 1 + compiler-core/checking2/src/source/derive.rs | 111 ++++++++++++ .../src/source/derive/contravariant.rs | 1 + .../checking2/src/source/derive/eq1_ord1.rs | 1 + .../checking2/src/source/derive/eq_ord.rs | 1 + .../checking2/src/source/derive/field.rs | 1 + .../checking2/src/source/derive/foldable.rs | 1 + .../checking2/src/source/derive/functor.rs | 1 + .../checking2/src/source/derive/generic.rs | 1 + .../checking2/src/source/derive/head.rs | 167 ++++++++++++++++++ .../checking2/src/source/derive/member.rs | 62 +++++++ .../checking2/src/source/derive/newtype.rs | 1 + .../checking2/src/source/derive/tools.rs | 1 + .../src/source/derive/traversable.rs | 1 + .../checking2/src/source/derive/variance.rs | 1 + .../checking2/src/source/term_items.rs | 4 +- 19 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 compiler-core/checking2/src/source/derive.rs create mode 100644 compiler-core/checking2/src/source/derive/contravariant.rs create mode 100644 compiler-core/checking2/src/source/derive/eq1_ord1.rs create mode 100644 compiler-core/checking2/src/source/derive/eq_ord.rs create mode 100644 compiler-core/checking2/src/source/derive/field.rs create mode 100644 compiler-core/checking2/src/source/derive/foldable.rs create mode 100644 compiler-core/checking2/src/source/derive/functor.rs create mode 100644 compiler-core/checking2/src/source/derive/generic.rs create mode 100644 compiler-core/checking2/src/source/derive/head.rs create mode 100644 compiler-core/checking2/src/source/derive/member.rs create mode 100644 compiler-core/checking2/src/source/derive/newtype.rs create mode 100644 compiler-core/checking2/src/source/derive/tools.rs create mode 100644 compiler-core/checking2/src/source/derive/traversable.rs create mode 100644 compiler-core/checking2/src/source/derive/variance.rs diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index b0e23532..8ea56f08 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -103,6 +103,15 @@ fn collect_instances_from_checked( checked: checked.clone(), }), ); + + output.extend( + checked + .derived + .values() + .filter(|instance| instance.resolution == (class_file, class_id)) + .cloned() + .map(|checked| CandidateInstance { chain_id: None, position: 0, checked }), + ); } fn get_functional_dependencies( diff --git a/compiler-core/checking2/src/error.rs b/compiler-core/checking2/src/error.rs index 855a405c..a9d5aa57 100644 --- a/compiler-core/checking2/src/error.rs +++ b/compiler-core/checking2/src/error.rs @@ -60,6 +60,10 @@ pub enum ErrorKind { expected: usize, actual: usize, }, + DeriveNotSupportedYet { + class_file: files::FileId, + class_id: indexing::TypeItemId, + }, DeriveMissingFunctor, EmptyAdoBlock, EmptyDoBlock, diff --git a/compiler-core/checking2/src/lib.rs b/compiler-core/checking2/src/lib.rs index 1c14ccd6..e72f0649 100644 --- a/compiler-core/checking2/src/lib.rs +++ b/compiler-core/checking2/src/lib.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use building_types::{QueryProxy, QueryResult}; use files::FileId; -use indexing::{InstanceId, TermItemId, TypeItemId}; +use indexing::{DeriveId, InstanceId, TermItemId, TypeItemId}; use resolving::ResolvedModule; use rustc_hash::FxHashMap; use smol_str::SmolStr; @@ -61,6 +61,7 @@ pub struct CheckedModule { pub synonyms: FxHashMap, pub classes: FxHashMap, pub instances: FxHashMap, + pub derived: FxHashMap, pub roles: FxHashMap>, pub nodes: CheckedNodes, pub errors: Vec, @@ -107,6 +108,10 @@ impl CheckedModule { self.instances.get(&id).cloned() } + pub fn lookup_derived(&self, id: DeriveId) -> Option { + self.derived.get(&id).cloned() + } + pub fn lookup_roles(&self, id: TypeItemId) -> Option> { self.roles.get(&id).cloned() } diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index deecf04a..22e1493b 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -8,6 +8,7 @@ pub mod synonym; pub mod terms; pub mod types; +mod derive; mod term_items; mod type_items; diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs new file mode 100644 index 00000000..cfdbdde5 --- /dev/null +++ b/compiler-core/checking2/src/source/derive.rs @@ -0,0 +1,111 @@ +pub mod head; +pub mod member; + +pub mod contravariant; +pub mod eq1_ord1; +pub mod eq_ord; +pub mod field; +pub mod foldable; +pub mod functor; +pub mod generic; +pub mod newtype; +pub mod tools; +pub mod traversable; +pub mod variance; + +use building_types::QueryResult; +use files::FileId; +use indexing::{TermItemId, TypeItemId}; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::state::CheckState; + +#[derive(Clone, Copy)] +enum DeriveDispatch { + Eq, + Eq1, + Ord, + Ord1, + Functor, + Bifunctor, + Contravariant, + Profunctor, + Foldable, + Bifoldable, + Traversable, + Bitraversable, + Newtype, + Generic, + Unsupported, +} + +pub struct DeriveHeadResult { + item_id: TermItemId, + class_file: FileId, + class_id: TypeItemId, + dispatch: DeriveDispatch, +} + +fn derive_dispatch( + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, +) -> DeriveDispatch +where + Q: ExternalQueries, +{ + let class = Some((class_file, class_id)); + if class == context.known_types.eq { + DeriveDispatch::Eq + } else if class == context.known_types.eq1 { + DeriveDispatch::Eq1 + } else if class == context.known_types.ord { + DeriveDispatch::Ord + } else if class == context.known_types.ord1 { + DeriveDispatch::Ord1 + } else if class == context.known_types.functor { + DeriveDispatch::Functor + } else if class == context.known_types.bifunctor { + DeriveDispatch::Bifunctor + } else if class == context.known_types.contravariant { + DeriveDispatch::Contravariant + } else if class == context.known_types.profunctor { + DeriveDispatch::Profunctor + } else if class == context.known_types.foldable { + DeriveDispatch::Foldable + } else if class == context.known_types.bifoldable { + DeriveDispatch::Bifoldable + } else if class == context.known_types.traversable { + DeriveDispatch::Traversable + } else if class == context.known_types.bitraversable { + DeriveDispatch::Bitraversable + } else if class == context.known_types.newtype { + DeriveDispatch::Newtype + } else if class == context.known_types.generic { + DeriveDispatch::Generic + } else { + DeriveDispatch::Unsupported + } +} + +pub fn check_derive_declarations( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult> +where + Q: ExternalQueries, +{ + head::check_derive_declarations(state, context) +} + +pub fn check_derive_members( + state: &mut CheckState, + context: &CheckContext, + derives: &[DeriveHeadResult], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + member::check_derive_members(state, context, derives) +} diff --git a/compiler-core/checking2/src/source/derive/contravariant.rs b/compiler-core/checking2/src/source/derive/contravariant.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/contravariant.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/eq1_ord1.rs b/compiler-core/checking2/src/source/derive/eq1_ord1.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/eq1_ord1.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/eq_ord.rs b/compiler-core/checking2/src/source/derive/eq_ord.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/eq_ord.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/foldable.rs b/compiler-core/checking2/src/source/derive/foldable.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/foldable.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/functor.rs b/compiler-core/checking2/src/source/derive/functor.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/functor.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs new file mode 100644 index 00000000..d43fcbbc --- /dev/null +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -0,0 +1,167 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::{TermItemId, TermItemKind, TypeItemId}; +use lowering::TermItemIr; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{CheckedInstance, Type, constraint, generalise, toolkit, unification, zonk}; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::types; +use crate::state::CheckState; + +use super::{DeriveHeadResult, derive_dispatch}; + +pub fn check_derive_declarations( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut results = vec![]; + + for scc in &context.grouped.term_scc { + let items = scc.as_slice(); + + let items = items.iter().filter_map(|&item_id| { + let item = context.lowered.info.get_term_item(item_id)?; + let TermItemIr::Derive { constraints, resolution, arguments, .. } = item else { + return None; + }; + let resolution = *resolution; + Some(CheckDeriveDeclaration { item_id, constraints, resolution, arguments }) + }); + + for item in items { + if let Some(result) = check_derive_declaration(state, context, item)? { + results.push(result); + } + } + } + + Ok(results) +} + +struct CheckDeriveDeclaration<'a> { + item_id: TermItemId, + constraints: &'a [lowering::TypeId], + resolution: Option<(FileId, TypeItemId)>, + arguments: &'a [lowering::TypeId], +} + +struct CheckDeriveDeclarationCore<'a> { + derive_id: indexing::DeriveId, + item_id: TermItemId, + class_file: FileId, + class_id: TypeItemId, + constraints: &'a [lowering::TypeId], + arguments: &'a [lowering::TypeId], +} + +fn check_derive_declaration( + state: &mut CheckState, + context: &CheckContext, + item: CheckDeriveDeclaration, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let CheckDeriveDeclaration { item_id, constraints, resolution, arguments } = item; + + let Some((class_file, class_id)) = resolution else { + return Ok(None); + }; + + let TermItemKind::Derive { id: derive_id } = context.indexed.items[item_id].kind else { + return Ok(None); + }; + + state.with_error_crumb(ErrorCrumb::TermDeclaration(item_id), |state| { + check_derive_declaration_core(state, context, CheckDeriveDeclarationCore { + derive_id, + item_id, + class_file, + class_id, + constraints, + arguments, + }) + }) +} + +fn check_derive_declaration_core( + state: &mut CheckState, + context: &CheckContext, + item: CheckDeriveDeclarationCore, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let CheckDeriveDeclarationCore { + derive_id, + item_id, + class_file, + class_id, + constraints, + arguments, + } = item; + + let class_kind = toolkit::lookup_file_type(state, context, class_file, class_id)?; + + let expected_kinds = { + let toolkit::InspectQuantified { quantified, .. } = + toolkit::inspect_quantified(state, context, class_kind)?; + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, quantified)?; + arguments + }; + + if expected_kinds.len() != arguments.len() { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: expected_kinds.len(), + actual: arguments.len(), + }); + } + + let mut class_type = context.queries.intern_type(Type::Constructor(class_file, class_id)); + let mut class_kind = class_kind; + let mut checked_arguments = Vec::with_capacity(arguments.len()); + + for &argument in arguments { + (class_type, class_kind) = + types::infer_application_kind(state, context, (class_type, class_kind), argument)?; + let (_, extracted_arguments) = + toolkit::extract_type_application(state, context, class_type)?; + if let Some(&checked_argument) = extracted_arguments.last() { + checked_arguments.push(checked_argument); + } + } + + unification::subtype(state, context, class_kind, context.prim.constraint)?; + + let mut checked_constraints = Vec::with_capacity(constraints.len()); + for &constraint in constraints { + let (constraint_type, _) = + types::check_kind(state, context, constraint, context.prim.constraint)?; + checked_constraints.push(constraint_type); + } + + let mut canonical = class_type; + for &constraint in checked_constraints.iter().rev() { + canonical = context.intern_constrained(constraint, canonical); + } + + constraint::instances::validate_rows(state, context, class_file, class_id, &checked_arguments)?; + + let resolution = (class_file, class_id); + let canonical = zonk::zonk(state, context, canonical)?; + let canonical = generalise::generalise_implicit(state, context, canonical)?; + + state.checked.derived.insert(derive_id, CheckedInstance { resolution, canonical }); + + let dispatch = derive_dispatch(context, class_file, class_id); + + Ok(Some(DeriveHeadResult { item_id, class_file, class_id, dispatch })) +} diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs new file mode 100644 index 00000000..33814a47 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -0,0 +1,62 @@ +use building_types::QueryResult; + +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::error::{ErrorCrumb, ErrorKind}; +use crate::state::CheckState; + +use super::{DeriveDispatch, DeriveHeadResult}; + +pub fn check_derive_members( + state: &mut CheckState, + context: &CheckContext, + derives: &[DeriveHeadResult], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for result in derives { + state.with_error_crumb(ErrorCrumb::TermDeclaration(result.item_id), |state| { + check_derive_member(state, context, result) + })?; + } + Ok(()) +} + +fn check_derive_member( + state: &mut CheckState, + _context: &CheckContext, + result: &DeriveHeadResult, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + match result.dispatch { + DeriveDispatch::Eq + | DeriveDispatch::Eq1 + | DeriveDispatch::Ord + | DeriveDispatch::Ord1 + | DeriveDispatch::Functor + | DeriveDispatch::Bifunctor + | DeriveDispatch::Contravariant + | DeriveDispatch::Profunctor + | DeriveDispatch::Foldable + | DeriveDispatch::Bifoldable + | DeriveDispatch::Traversable + | DeriveDispatch::Bitraversable + | DeriveDispatch::Newtype + | DeriveDispatch::Generic => { + state.insert_error(ErrorKind::DeriveNotSupportedYet { + class_file: result.class_file, + class_id: result.class_id, + }); + } + DeriveDispatch::Unsupported => { + state.insert_error(ErrorKind::CannotDeriveClass { + class_file: result.class_file, + class_id: result.class_id, + }); + } + } + Ok(()) +} diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/tools.rs b/compiler-core/checking2/src/source/derive/tools.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/tools.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/traversable.rs b/compiler-core/checking2/src/source/derive/traversable.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/traversable.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -0,0 +1 @@ + diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index eb80e3a2..18b0d443 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -14,7 +14,7 @@ use crate::core::{ }; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::terms::equations; -use crate::source::types; +use crate::source::{derive, types}; use crate::state::CheckState; #[derive(Default)] @@ -33,8 +33,10 @@ where Q: ExternalQueries, { check_instance_declarations(state, context)?; + let derive_results = derive::check_derive_declarations(state, context)?; check_value_groups(state, context)?; check_instance_members(state, context)?; + derive::check_derive_members(state, context, &derive_results)?; Ok(()) } From 6f9998374ae75d388f98d7089e40c56973511de9 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 02:55:42 +0800 Subject: [PATCH 304/386] Add initial smoke test for Eq deriving --- .../082_derive_pipeline_smoke/Main.purs | 10 +++++++ .../082_derive_pipeline_smoke/Main.snap | 27 +++++++++++++++++++ .../tests/checking2/generated.rs | 2 ++ 3 files changed, 39 insertions(+) create mode 100644 tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.purs create mode 100644 tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap diff --git a/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.purs b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.purs new file mode 100644 index 00000000..660bf3b6 --- /dev/null +++ b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Eq (class Eq, eq) + +data Box = Box + +derive instance Eq Box + +test :: Boolean +test = eq Box Box diff --git a/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap new file mode 100644 index 00000000..086009f3 --- /dev/null +++ b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: Box +test :: Boolean + +Types +Box :: Type + +Roles +Box = [] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 92dd48eb..7db9afd5 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -191,3 +191,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_080_instance_member_higher_kinded_main() { run_test("080_instance_member_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_081_instance_member_too_many_binders_main() { run_test("081_instance_member_too_many_binders", "Main"); } + +#[rustfmt::skip] #[test] fn test_082_derive_pipeline_smoke_main() { run_test("082_derive_pipeline_smoke", "Main"); } From 1321b1536695739da818bcb490fe1c47587338af Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 12:35:39 +0800 Subject: [PATCH 305/386] Add smoke tests for deriving Eq --- .../checking2/083_derive_eq_simple/Main.purs | 13 ++++++ .../checking2/083_derive_eq_simple/Main.snap | 43 +++++++++++++++++++ .../084_derive_eq_parameterized/Main.purs | 7 +++ .../084_derive_eq_parameterized/Main.snap | 27 ++++++++++++ .../085_derive_eq_missing_instance/Main.purs | 9 ++++ .../085_derive_eq_missing_instance/Main.snap | 29 +++++++++++++ .../tests/checking2/generated.rs | 6 +++ 7 files changed, 134 insertions(+) create mode 100644 tests-integration/fixtures/checking2/083_derive_eq_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.purs create mode 100644 tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap create mode 100644 tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.purs create mode 100644 tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap diff --git a/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.purs b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.purs new file mode 100644 index 00000000..a2d03a12 --- /dev/null +++ b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.purs @@ -0,0 +1,13 @@ +module Main where + +import Data.Eq (class Eq) + +data Proxy a = Proxy + +derive instance Eq (Proxy a) + +data NoEq = MkNoEq + +data ContainsNoEq = MkContainsNoEq NoEq + +derive instance Eq ContainsNoEq diff --git a/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap new file mode 100644 index 00000000..84e96bd6 --- /dev/null +++ b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap @@ -0,0 +1,43 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 :: Type)) +MkNoEq :: NoEq +MkContainsNoEq :: NoEq -> ContainsNoEq + +Types +Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type +NoEq :: Type +ContainsNoEq :: Type + +Roles +Proxy = [Phantom] +NoEq = [] +ContainsNoEq = [] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.purs b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.purs new file mode 100644 index 00000000..02d3d137 --- /dev/null +++ b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Eq (class Eq) + +data Maybe a = Nothing | Just a + +derive instance Eq a => Eq (Maybe a) diff --git a/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap new file mode 100644 index 00000000..e547b24d --- /dev/null +++ b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.purs b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.purs new file mode 100644 index 00000000..8aaea864 --- /dev/null +++ b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Eq (class Eq) + +data NoEq = MkNoEq + +data Box = MkBox NoEq + +derive instance Eq Box diff --git a/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap new file mode 100644 index 00000000..3dd85b98 --- /dev/null +++ b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +MkNoEq :: NoEq +MkBox :: NoEq -> Box + +Types +NoEq :: Type +Box :: Type + +Roles +NoEq = [] +Box = [] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 7db9afd5..b1241bba 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -193,3 +193,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_081_instance_member_too_many_binders_main() { run_test("081_instance_member_too_many_binders", "Main"); } #[rustfmt::skip] #[test] fn test_082_derive_pipeline_smoke_main() { run_test("082_derive_pipeline_smoke", "Main"); } + +#[rustfmt::skip] #[test] fn test_083_derive_eq_simple_main() { run_test("083_derive_eq_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_084_derive_eq_parameterized_main() { run_test("084_derive_eq_parameterized", "Main"); } + +#[rustfmt::skip] #[test] fn test_085_derive_eq_missing_instance_main() { run_test("085_derive_eq_missing_instance", "Main"); } From d9c302aaade970b7ed09d1c2c62a3d579d20bbab Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 12:36:05 +0800 Subject: [PATCH 306/386] Implement deriving for Eq --- compiler-core/checking2/src/source/derive.rs | 71 +++++----- .../checking2/src/source/derive/eq_ord.rs | 45 ++++++ .../checking2/src/source/derive/field.rs | 130 ++++++++++++++++++ .../checking2/src/source/derive/head.rs | 26 +++- .../checking2/src/source/derive/member.rs | 51 +++---- .../checking2/src/source/derive/tools.rs | 87 ++++++++++++ .../082_derive_pipeline_smoke/Main.snap | 13 -- .../checking2/083_derive_eq_simple/Main.snap | 16 +-- .../084_derive_eq_parameterized/Main.snap | 13 -- .../085_derive_eq_missing_instance/Main.snap | 5 +- 10 files changed, 347 insertions(+), 110 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index cfdbdde5..ce021726 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -17,6 +17,7 @@ use building_types::QueryResult; use files::FileId; use indexing::{TermItemId, TypeItemId}; +use crate::core::TypeId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::state::CheckState; @@ -24,27 +25,28 @@ use crate::state::CheckState; #[derive(Clone, Copy)] enum DeriveDispatch { Eq, - Eq1, - Ord, - Ord1, - Functor, - Bifunctor, - Contravariant, - Profunctor, - Foldable, - Bifoldable, - Traversable, - Bitraversable, - Newtype, - Generic, + SupportedButNotImplemented, + Unsupported, +} + +#[derive(Clone, Copy)] +pub(super) enum DeriveStrategy { + FieldConstraints { + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + class: (FileId, TypeItemId), + }, Unsupported, } pub struct DeriveHeadResult { item_id: TermItemId, + constraints: Vec, class_file: FileId, class_id: TypeItemId, - dispatch: DeriveDispatch, + arguments: Vec, + strategy: DeriveStrategy, } fn derive_dispatch( @@ -58,32 +60,21 @@ where let class = Some((class_file, class_id)); if class == context.known_types.eq { DeriveDispatch::Eq - } else if class == context.known_types.eq1 { - DeriveDispatch::Eq1 - } else if class == context.known_types.ord { - DeriveDispatch::Ord - } else if class == context.known_types.ord1 { - DeriveDispatch::Ord1 - } else if class == context.known_types.functor { - DeriveDispatch::Functor - } else if class == context.known_types.bifunctor { - DeriveDispatch::Bifunctor - } else if class == context.known_types.contravariant { - DeriveDispatch::Contravariant - } else if class == context.known_types.profunctor { - DeriveDispatch::Profunctor - } else if class == context.known_types.foldable { - DeriveDispatch::Foldable - } else if class == context.known_types.bifoldable { - DeriveDispatch::Bifoldable - } else if class == context.known_types.traversable { - DeriveDispatch::Traversable - } else if class == context.known_types.bitraversable { - DeriveDispatch::Bitraversable - } else if class == context.known_types.newtype { - DeriveDispatch::Newtype - } else if class == context.known_types.generic { - DeriveDispatch::Generic + } else if class == context.known_types.eq1 + || class == context.known_types.ord + || class == context.known_types.ord1 + || class == context.known_types.functor + || class == context.known_types.bifunctor + || class == context.known_types.contravariant + || class == context.known_types.profunctor + || class == context.known_types.foldable + || class == context.known_types.bifoldable + || class == context.known_types.traversable + || class == context.known_types.bitraversable + || class == context.known_types.newtype + || class == context.known_types.generic + { + DeriveDispatch::SupportedButNotImplemented } else { DeriveDispatch::Unsupported } diff --git a/compiler-core/checking2/src/source/derive/eq_ord.rs b/compiler-core/checking2/src/source/derive/eq_ord.rs index 8b137891..92b21ab7 100644 --- a/compiler-core/checking2/src/source/derive/eq_ord.rs +++ b/compiler-core/checking2/src/source/derive/eq_ord.rs @@ -1 +1,46 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; + +pub(super) fn check_derive_eq( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + Ok(Some(DeriveStrategy::FieldConstraints { + data_file, + data_id, + derived_type: *derived_type, + class: (class_file, class_id), + })) +} diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs index 8b137891..b7d742ae 100644 --- a/compiler-core/checking2/src/source/derive/field.rs +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -1 +1,131 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, toolkit}; +use crate::state::CheckState; + +use super::tools; + +pub(super) fn generate_field_constraints( + state: &mut CheckState, + context: &CheckContext, + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + class: (FileId, TypeItemId), +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let class1 = if context.known_types.eq == Some(class) { + context.known_types.eq1 + } else if context.known_types.ord == Some(class) { + context.known_types.ord1 + } else { + None + }; + + let (_, arguments) = toolkit::extract_type_application(state, context, derived_type)?; + + for constructor_id in tools::lookup_data_constructors(context, data_file, data_id)? { + let constructor_t = toolkit::lookup_file_term(state, context, data_file, constructor_id)?; + let field_types = instantiate_constructor_fields(state, context, constructor_t, &arguments)?; + for field_type in field_types { + generate_constraint(state, context, field_type, class, class1)?; + } + } + + Ok(()) +} + +fn instantiate_constructor_fields( + state: &mut CheckState, + context: &CheckContext, + constructor_t: TypeId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut current = constructor_t; + let mut arguments = arguments.iter().copied(); + + loop { + current = normalise::normalise(state, context, current)?; + let Type::Forall(binder_id, inner) = context.lookup_type(current) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let replacement = arguments.next().unwrap_or_else(|| { + state.fresh_rigid(context.queries, binder.kind) + }); + current = SubstituteName::one(state, context, binder.name, replacement, inner)?; + } + + let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, current)?; + Ok(arguments) +} + +fn generate_constraint( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, + class: (FileId, TypeItemId), + class1: Option<(FileId, TypeItemId)>, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let type_id = normalise::normalise(state, context, type_id)?; + + match context.lookup_type(type_id) { + Type::Application(function, argument) => { + let function = normalise::normalise(state, context, function)?; + if function == context.prim.record { + generate_constraint(state, context, argument, class, class1)?; + } else if is_type_to_type_variable(state, context, function)? { + if let Some(class1) = class1 { + tools::emit_constraint(context, state, class1, function); + } + tools::emit_constraint(context, state, class, argument); + } else { + tools::emit_constraint(context, state, class, type_id); + } + } + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + for field in row.fields.iter() { + generate_constraint(state, context, field.id, class, class1)?; + } + if let Some(tail) = row.tail { + generate_constraint(state, context, tail, class, class1)?; + } + } + _ => tools::emit_constraint(context, state, class, type_id), + } + + Ok(()) +} + +fn is_type_to_type_variable( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let type_id = normalise::normalise(state, context, type_id)?; + let kind = match context.lookup_type(type_id) { + Type::Rigid(_, _, kind) => kind, + Type::Unification(unification_id) => state.unifications.get(unification_id).kind, + _ => return Ok(false), + }; + + Ok(normalise::normalise(state, context, kind)? == context.prim.type_to_type) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index d43fcbbc..7ba137c9 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -10,7 +10,7 @@ use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::types; use crate::state::CheckState; -use super::{DeriveHeadResult, derive_dispatch}; +use super::{DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq_ord}; pub fn check_derive_declarations( state: &mut CheckState, @@ -161,7 +161,27 @@ where state.checked.derived.insert(derive_id, CheckedInstance { resolution, canonical }); - let dispatch = derive_dispatch(context, class_file, class_id); + let strategy = match derive_dispatch(context, class_file, class_id) { + DeriveDispatch::Eq => eq_ord::check_derive_eq( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), + DeriveDispatch::Unsupported => { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + None + } + }; - Ok(Some(DeriveHeadResult { item_id, class_file, class_id, dispatch })) + Ok(strategy.map(|strategy| DeriveHeadResult { + item_id, + constraints: checked_constraints, + class_file, + class_id, + arguments: checked_arguments, + strategy, + })) } diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index 33814a47..385e4997 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -5,7 +5,7 @@ use crate::context::CheckContext; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; -use super::{DeriveDispatch, DeriveHeadResult}; +use super::{DeriveHeadResult, DeriveStrategy, field, tools}; pub fn check_derive_members( state: &mut CheckState, @@ -17,7 +17,7 @@ where { for result in derives { state.with_error_crumb(ErrorCrumb::TermDeclaration(result.item_id), |state| { - check_derive_member(state, context, result) + state.with_implication(|state| check_derive_member(state, context, result)) })?; } Ok(()) @@ -25,34 +25,37 @@ where fn check_derive_member( state: &mut CheckState, - _context: &CheckContext, + context: &CheckContext, result: &DeriveHeadResult, ) -> QueryResult<()> where Q: ExternalQueries, { - match result.dispatch { - DeriveDispatch::Eq - | DeriveDispatch::Eq1 - | DeriveDispatch::Ord - | DeriveDispatch::Ord1 - | DeriveDispatch::Functor - | DeriveDispatch::Bifunctor - | DeriveDispatch::Contravariant - | DeriveDispatch::Profunctor - | DeriveDispatch::Foldable - | DeriveDispatch::Bifoldable - | DeriveDispatch::Traversable - | DeriveDispatch::Bitraversable - | DeriveDispatch::Newtype - | DeriveDispatch::Generic => { - state.insert_error(ErrorKind::DeriveNotSupportedYet { - class_file: result.class_file, - class_id: result.class_id, - }); + for &constraint in &result.constraints { + state.push_given(constraint); + } + + match result.strategy { + DeriveStrategy::FieldConstraints { data_file, data_id, derived_type, class } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + field::generate_field_constraints( + state, + context, + data_file, + data_id, + derived_type, + class, + )?; + tools::solve_and_report_constraints(state, context)?; } - DeriveDispatch::Unsupported => { - state.insert_error(ErrorKind::CannotDeriveClass { + DeriveStrategy::Unsupported => { + state.insert_error(ErrorKind::DeriveNotSupportedYet { class_file: result.class_file, class_id: result.class_id, }); diff --git a/compiler-core/checking2/src/source/derive/tools.rs b/compiler-core/checking2/src/source/derive/tools.rs index 8b137891..1cdfcd6f 100644 --- a/compiler-core/checking2/src/source/derive/tools.rs +++ b/compiler-core/checking2/src/source/derive/tools.rs @@ -1 +1,88 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::{TermItemId, TypeItemId}; +use rustc_hash::FxHashMap; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{CheckedClass, Type, TypeId, toolkit}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +pub(super) fn emit_constraint( + context: &CheckContext, + state: &mut CheckState, + class: (FileId, TypeItemId), + argument: TypeId, +) where + Q: ExternalQueries, +{ + let class_t = context.queries.intern_type(Type::Constructor(class.0, class.1)); + state.push_wanted(context.intern_application(class_t, argument)); +} + +pub(super) fn emit_superclass_constraints( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[TypeId], +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let Some(CheckedClass { type_parameters, superclasses, .. }) = + toolkit::lookup_file_class(state, context, class_file, class_id)? + else { + return Ok(()); + }; + + if superclasses.is_empty() { + return Ok(()); + } + + let mut bindings = FxHashMap::default(); + for (binder_id, &argument) in type_parameters.iter().zip(arguments.iter()) { + let binder = context.lookup_forall_binder(*binder_id); + bindings.insert(binder.name, argument); + } + + for superclass in superclasses { + let specialised = SubstituteName::many(state, context, &bindings, superclass)?; + state.push_wanted(specialised); + } + + Ok(()) +} + +pub(super) fn lookup_data_constructors( + context: &CheckContext, + data_file: FileId, + data_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + if data_file == context.id { + Ok(context.indexed.pairs.data_constructors(data_id).collect()) + } else { + let indexed = context.queries.indexed(data_file)?; + Ok(indexed.pairs.data_constructors(data_id).collect()) + } +} + +pub(super) fn solve_and_report_constraints( + state: &mut CheckState, + context: &CheckContext, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let residual = state.solve_constraints(context)?; + for constraint in residual { + let constraint = state.pretty_id(context, constraint)?; + state.insert_error(ErrorKind::NoInstanceFound { constraint }); + } + Ok(()) +} diff --git a/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap index 086009f3..61e4d042 100644 --- a/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap +++ b/tests-integration/fixtures/checking2/082_derive_pipeline_smoke/Main.snap @@ -12,16 +12,3 @@ Box :: Type Roles Box = [] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap index 84e96bd6..493aba56 100644 --- a/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap +++ b/tests-integration/fixtures/checking2/083_derive_eq_simple/Main.snap @@ -20,20 +20,8 @@ ContainsNoEq = [] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(8), }, crumbs: [ TermDeclaration( diff --git a/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap index e547b24d..0c24dbcf 100644 --- a/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap +++ b/tests-integration/fixtures/checking2/084_derive_eq_parameterized/Main.snap @@ -12,16 +12,3 @@ Maybe :: Type -> Type Roles Maybe = [Representational] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap index 3dd85b98..e85e88fc 100644 --- a/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap +++ b/tests-integration/fixtures/checking2/085_derive_eq_missing_instance/Main.snap @@ -17,9 +17,8 @@ Box = [] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(8), }, crumbs: [ TermDeclaration( From d4d6cec5ea6cb81884f5d6b9fd271e1cc1313d58 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:19:32 +0800 Subject: [PATCH 307/386] Add smoke tests for deriving Ord --- .../checking2/086_derive_ord_simple/Main.purs | 23 +++++++ .../checking2/086_derive_ord_simple/Main.snap | 57 ++++++++++++++++ .../087_derive_ord_1_higher_kinded/Main.purs | 14 ++++ .../087_derive_ord_1_higher_kinded/Main.snap | 66 +++++++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 164 insertions(+) create mode 100644 tests-integration/fixtures/checking2/086_derive_ord_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap diff --git a/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.purs b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.purs new file mode 100644 index 00000000..210b310f --- /dev/null +++ b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.purs @@ -0,0 +1,23 @@ +module Main where + +import Data.Eq (class Eq) +import Data.Ord (class Ord) + +data Box = MkBox Int + +derive instance Eq Box +derive instance Ord Box + +data Pair = MkPair Int Boolean + +derive instance Eq Pair +derive instance Ord Pair + +data NoOrd = MkNoOrd + +derive instance Eq NoOrd + +data ContainsNoOrd = MkContainsNoOrd NoOrd + +derive instance Eq ContainsNoOrd +derive instance Ord ContainsNoOrd diff --git a/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap new file mode 100644 index 00000000..5f4629ad --- /dev/null +++ b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +MkBox :: Int -> Box +MkPair :: Int -> Boolean -> Pair +MkNoOrd :: NoOrd +MkContainsNoOrd :: NoOrd -> ContainsNoOrd + +Types +Box :: Type +Pair :: Type +NoOrd :: Type +ContainsNoOrd :: Type + +Roles +Box = [] +Pair = [] +NoOrd = [] +ContainsNoOrd = [] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(10), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.purs new file mode 100644 index 00000000..85ac1a72 --- /dev/null +++ b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.purs @@ -0,0 +1,14 @@ +module Main where + +import Data.Eq (class Eq, class Eq1) +import Data.Ord (class Ord, class Ord1) + +data Wrap f a = MkWrap (f a) + +derive instance (Eq1 f, Eq a) => Eq (Wrap f a) +derive instance (Ord1 f, Ord a) => Ord (Wrap f a) + +data WrapNoOrd1 f a = MkWrapNoOrd1 (f a) + +derive instance (Eq1 f, Eq a) => Eq (WrapNoOrd1 f a) +derive instance Ord a => Ord (WrapNoOrd1 f a) diff --git a/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap new file mode 100644 index 00000000..99452c78 --- /dev/null +++ b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap @@ -0,0 +1,66 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +MkWrap :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + Wrap @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) +MkWrapNoOrd1 :: + forall (t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + WrapNoOrd1 @(t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) + +Types +Wrap :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type +WrapNoOrd1 :: forall (t5 :: Type). ((t5 :: Type) -> Type) -> (t5 :: Type) -> Type + +Roles +Wrap = [Representational, Nominal] +WrapNoOrd1 = [Representational, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index b1241bba..838d0996 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -199,3 +199,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_084_derive_eq_parameterized_main() { run_test("084_derive_eq_parameterized", "Main"); } #[rustfmt::skip] #[test] fn test_085_derive_eq_missing_instance_main() { run_test("085_derive_eq_missing_instance", "Main"); } + +#[rustfmt::skip] #[test] fn test_086_derive_ord_simple_main() { run_test("086_derive_ord_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_087_derive_ord_1_higher_kinded_main() { run_test("087_derive_ord_1_higher_kinded", "Main"); } From 0ae21f32e5c5a443826aa50a1f23199f609d5f22 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:20:00 +0800 Subject: [PATCH 308/386] Implement deriving for Ord --- compiler-core/checking2/src/source/derive.rs | 4 ++- .../checking2/src/source/derive/eq_ord.rs | 26 ++++++++++++++++++ .../checking2/src/source/derive/head.rs | 7 +++++ .../checking2/086_derive_ord_simple/Main.snap | 27 ++----------------- .../087_derive_ord_1_higher_kinded/Main.snap | 22 ++++++++++----- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index ce021726..77572b77 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -25,6 +25,7 @@ use crate::state::CheckState; #[derive(Clone, Copy)] enum DeriveDispatch { Eq, + Ord, SupportedButNotImplemented, Unsupported, } @@ -60,8 +61,9 @@ where let class = Some((class_file, class_id)); if class == context.known_types.eq { DeriveDispatch::Eq + } else if class == context.known_types.ord { + DeriveDispatch::Ord } else if class == context.known_types.eq1 - || class == context.known_types.ord || class == context.known_types.ord1 || class == context.known_types.functor || class == context.known_types.bifunctor diff --git a/compiler-core/checking2/src/source/derive/eq_ord.rs b/compiler-core/checking2/src/source/derive/eq_ord.rs index 92b21ab7..a399fa0a 100644 --- a/compiler-core/checking2/src/source/derive/eq_ord.rs +++ b/compiler-core/checking2/src/source/derive/eq_ord.rs @@ -17,6 +17,32 @@ pub(super) fn check_derive_eq( class_id: TypeItemId, arguments: &[crate::core::TypeId], ) -> QueryResult> +where + Q: ExternalQueries, +{ + check_derive_field_constraints(state, context, class_file, class_id, arguments) +} + +pub(super) fn check_derive_ord( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + check_derive_field_constraints(state, context, class_file, class_id, arguments) +} + +fn check_derive_field_constraints( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> where Q: ExternalQueries, { diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 7ba137c9..4dbbae2f 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -169,6 +169,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Ord => eq_ord::check_derive_ord( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), DeriveDispatch::Unsupported => { state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); diff --git a/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap index 5f4629ad..de637e82 100644 --- a/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap +++ b/tests-integration/fixtures/checking2/086_derive_ord_simple/Main.snap @@ -23,31 +23,8 @@ ContainsNoOrd = [] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(1), + kind: NoInstanceFound { + constraint: Id(8), }, crumbs: [ TermDeclaration( diff --git a/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap index 99452c78..58e5dc0d 100644 --- a/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/087_derive_ord_1_higher_kinded/Main.snap @@ -33,9 +33,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(1), + kind: NoInstanceFound { + constraint: Id(9), }, crumbs: [ TermDeclaration( @@ -45,7 +44,7 @@ CheckError { } CheckError { kind: NoInstanceFound { - constraint: Id(9), + constraint: Id(10), }, crumbs: [ TermDeclaration( @@ -54,9 +53,18 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(1), + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), }, crumbs: [ TermDeclaration( From 8a11630da2a00b3fee11627e55a8338d4bff1d5b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:21:41 +0800 Subject: [PATCH 309/386] Add smoke tests for Eq1 --- .../checking2/088_derive_eq_1/Main.purs | 43 +++++ .../checking2/088_derive_eq_1/Main.snap | 157 ++++++++++++++++++ .../tests/checking2/generated.rs | 2 + 3 files changed, 202 insertions(+) create mode 100644 tests-integration/fixtures/checking2/088_derive_eq_1/Main.purs create mode 100644 tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap diff --git a/tests-integration/fixtures/checking2/088_derive_eq_1/Main.purs b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.purs new file mode 100644 index 00000000..8ed9a364 --- /dev/null +++ b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.purs @@ -0,0 +1,43 @@ +module Main where + +import Data.Eq (class Eq, class Eq1) + +data Id a = Id a + +derive instance Eq a => Eq (Id a) +derive instance Eq1 Id + +data Pair a = Pair a a + +derive instance Eq a => Eq (Pair a) +derive instance Eq1 Pair + +data Mixed a = Mixed Int a Boolean + +derive instance Eq a => Eq (Mixed a) +derive instance Eq1 Mixed + +data Rec a = Rec { value :: a, count :: Int } + +derive instance Eq a => Eq (Rec a) +derive instance Eq1 Rec + +data Wrap f a = Wrap (f a) + +derive instance (Eq1 f, Eq a) => Eq (Wrap f a) +derive instance Eq1 f => Eq1 (Wrap f) + +data Compose f g a = Compose (f (g a)) + +derive instance (Eq1 f, Eq (g a)) => Eq (Compose f g a) + +derive instance Eq1 f => Eq1 (Compose f g) + +data Either' a = Left' a | Right' a + +derive instance Eq a => Eq (Either' a) +derive instance Eq1 Either' + +data NoEq a = NoEq a + +derive instance Eq1 NoEq diff --git a/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap new file mode 100644 index 00000000..bb21d34a --- /dev/null +++ b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap @@ -0,0 +1,157 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Id :: forall (a :: Type). (a :: Type) -> Id (a :: Type) +Pair :: forall (a :: Type). (a :: Type) -> (a :: Type) -> Pair (a :: Type) +Mixed :: forall (a :: Type). Int -> (a :: Type) -> Boolean -> Mixed (a :: Type) +Rec :: forall (a :: Type). { count :: Int, value :: (a :: Type) } -> Rec (a :: Type) +Wrap :: + forall (t6 :: Type) (f :: (t6 :: Type) -> Type) (a :: (t6 :: Type)). + (f :: (t6 :: Type) -> Type) (a :: (t6 :: Type)) -> + Wrap @(t6 :: Type) (f :: (t6 :: Type) -> Type) (a :: (t6 :: Type)) +Compose :: + forall (t11 :: Type) (t10 :: Type) (f :: (t11 :: Type) -> Type) + (g :: (t10 :: Type) -> (t11 :: Type)) (a :: (t10 :: Type)). + (f :: (t11 :: Type) -> Type) ((g :: (t10 :: Type) -> (t11 :: Type)) (a :: (t10 :: Type))) -> + Compose @(t11 :: Type) @(t10 :: Type) + (f :: (t11 :: Type) -> Type) + (g :: (t10 :: Type) -> (t11 :: Type)) + (a :: (t10 :: Type)) +Left' :: forall (a :: Type). (a :: Type) -> Either' (a :: Type) +Right' :: forall (a :: Type). (a :: Type) -> Either' (a :: Type) +NoEq :: forall (a :: Type). (a :: Type) -> NoEq (a :: Type) + +Types +Id :: Type -> Type +Pair :: Type -> Type +Mixed :: Type -> Type +Rec :: Type -> Type +Wrap :: forall (t6 :: Type). ((t6 :: Type) -> Type) -> (t6 :: Type) -> Type +Compose :: + forall (t11 :: Type) (t10 :: Type). + ((t11 :: Type) -> Type) -> ((t10 :: Type) -> (t11 :: Type)) -> (t10 :: Type) -> Type +Either' :: Type -> Type +NoEq :: Type -> Type + +Roles +Id = [Representational] +Pair = [Representational] +Mixed = [Representational] +Rec = [Representational] +Wrap = [Representational, Nominal] +Compose = [Representational, Representational, Nominal] +Either' = [Representational] +NoEq = [Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(11), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(13), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(14), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(16), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(17), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(21), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(19), + class_id: Idx::(1), + }, + crumbs: [ + TermDeclaration( + Idx::(23), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 838d0996..f17283e2 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -203,3 +203,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_086_derive_ord_simple_main() { run_test("086_derive_ord_simple", "Main"); } #[rustfmt::skip] #[test] fn test_087_derive_ord_1_higher_kinded_main() { run_test("087_derive_ord_1_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_088_derive_eq_1_main() { run_test("088_derive_eq_1", "Main"); } From c1d99250c4ddff84f2f40729ee88271562db4f8e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:22:12 +0800 Subject: [PATCH 310/386] Implement deriving for Eq1 --- compiler-core/checking2/src/source/derive.rs | 10 ++- .../checking2/src/source/derive/eq1_ord1.rs | 61 +++++++++++++++ .../checking2/src/source/derive/head.rs | 9 ++- .../checking2/src/source/derive/member.rs | 31 ++++++++ .../checking2/088_derive_eq_1/Main.snap | 76 +------------------ 5 files changed, 112 insertions(+), 75 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 77572b77..6ef36af0 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -25,6 +25,7 @@ use crate::state::CheckState; #[derive(Clone, Copy)] enum DeriveDispatch { Eq, + Eq1, Ord, SupportedButNotImplemented, Unsupported, @@ -38,6 +39,10 @@ pub(super) enum DeriveStrategy { derived_type: TypeId, class: (FileId, TypeItemId), }, + DelegateConstraint { + derived_type: TypeId, + class: (FileId, TypeItemId), + }, Unsupported, } @@ -61,10 +66,11 @@ where let class = Some((class_file, class_id)); if class == context.known_types.eq { DeriveDispatch::Eq + } else if class == context.known_types.eq1 { + DeriveDispatch::Eq1 } else if class == context.known_types.ord { DeriveDispatch::Ord - } else if class == context.known_types.eq1 - || class == context.known_types.ord1 + } else if class == context.known_types.ord1 || class == context.known_types.functor || class == context.known_types.bifunctor || class == context.known_types.contravariant diff --git a/compiler-core/checking2/src/source/derive/eq1_ord1.rs b/compiler-core/checking2/src/source/derive/eq1_ord1.rs index 8b137891..6bef7d76 100644 --- a/compiler-core/checking2/src/source/derive/eq1_ord1.rs +++ b/compiler-core/checking2/src/source/derive/eq1_ord1.rs @@ -1 +1,62 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; + +pub(super) fn check_derive_eq1( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(eq) = context.known_types.eq else { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + return Ok(None); + }; + + check_derive_delegate_constraint(state, context, class_file, class_id, arguments, eq) +} + +fn check_derive_delegate_constraint( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], + class: (FileId, TypeItemId), +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + if toolkit::extract_type_constructor(state, context, *derived_type)?.is_none() { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + } + + Ok(Some(DeriveStrategy::DelegateConstraint { + derived_type: *derived_type, + class, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 4dbbae2f..46785468 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -10,7 +10,7 @@ use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::types; use crate::state::CheckState; -use super::{DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq_ord}; +use super::{DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq1_ord1, eq_ord}; pub fn check_derive_declarations( state: &mut CheckState, @@ -169,6 +169,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Eq1 => eq1_ord1::check_derive_eq1( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index 385e4997..14cda2c2 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -2,6 +2,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; +use crate::core::Type; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; @@ -54,6 +55,17 @@ where )?; tools::solve_and_report_constraints(state, context)?; } + DeriveStrategy::DelegateConstraint { derived_type, class } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + generate_delegate_constraint(state, context, derived_type, class); + tools::solve_and_report_constraints(state, context)?; + } DeriveStrategy::Unsupported => { state.insert_error(ErrorKind::DeriveNotSupportedYet { class_file: result.class_file, @@ -63,3 +75,22 @@ where } Ok(()) } + +fn generate_delegate_constraint( + state: &mut CheckState, + context: &CheckContext, + derived_type: crate::core::TypeId, + class: (files::FileId, indexing::TypeItemId), +) where + Q: ExternalQueries, +{ + let skolem_type = state.fresh_rigid(context.queries, context.prim.t); + let applied_type = context.intern_application(derived_type, skolem_type); + + let class_type = context.queries.intern_type(Type::Constructor(class.0, class.1)); + let given_constraint = context.intern_application(class_type, skolem_type); + let wanted_constraint = context.intern_application(class_type, applied_type); + + state.push_given(given_constraint); + state.push_wanted(wanted_constraint); +} diff --git a/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap index bb21d34a..b31be8ab 100644 --- a/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap +++ b/tests-integration/fixtures/checking2/088_derive_eq_1/Main.snap @@ -47,50 +47,6 @@ Either' = [Representational] NoEq = [Representational] Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(8), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(11), - ), - ], -} CheckError { kind: NoInstanceFound { constraint: Id(9), @@ -101,17 +57,6 @@ CheckError { ), ], } -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(14), - ), - ], -} CheckError { kind: NoInstanceFound { constraint: Id(10), @@ -123,9 +68,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), + kind: NoInstanceFound { + constraint: Id(11), }, crumbs: [ TermDeclaration( @@ -134,20 +78,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), - }, - crumbs: [ - TermDeclaration( - Idx::(21), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(19), - class_id: Idx::(1), + kind: NoInstanceFound { + constraint: Id(12), }, crumbs: [ TermDeclaration( From 67947b1cbf1f10f683ff8cf8940639dad617fa00 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:25:39 +0800 Subject: [PATCH 311/386] Add smoke tests for Ord1 --- .../checking2/089_derive_ord_1/Main.purs | 32 +++++ .../checking2/089_derive_ord_1/Main.snap | 130 ++++++++++++++++++ .../tests/checking2/generated.rs | 2 + 3 files changed, 164 insertions(+) create mode 100644 tests-integration/fixtures/checking2/089_derive_ord_1/Main.purs create mode 100644 tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap diff --git a/tests-integration/fixtures/checking2/089_derive_ord_1/Main.purs b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.purs new file mode 100644 index 00000000..ffcba757 --- /dev/null +++ b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.purs @@ -0,0 +1,32 @@ +module Main where + +import Data.Eq (class Eq, class Eq1) +import Data.Ord (class Ord, class Ord1) + +data Id a = Id a + +derive instance Eq a => Eq (Id a) +derive instance Eq1 Id +derive instance Ord a => Ord (Id a) +derive instance Ord1 Id + +data Wrap f a = Wrap (f a) + +derive instance (Eq1 f, Eq a) => Eq (Wrap f a) +derive instance Eq1 f => Eq1 (Wrap f) +derive instance (Ord1 f, Ord a) => Ord (Wrap f a) +derive instance Ord1 f => Ord1 (Wrap f) + +data Compose f g a = Compose (f (g a)) + +derive instance (Eq1 f, Eq (g a)) => Eq (Compose f g a) +derive instance (Ord1 f, Ord (g a)) => Ord (Compose f g a) + +derive instance Eq1 f => Eq1 (Compose f g) +derive instance Ord1 f => Ord1 (Compose f g) + +data NoOrd a = NoOrd a + +derive instance Eq a => Eq (NoOrd a) +derive instance Eq1 NoOrd +derive instance Ord1 NoOrd diff --git a/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap new file mode 100644 index 00000000..b115c1b9 --- /dev/null +++ b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap @@ -0,0 +1,130 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Id :: forall (a :: Type). (a :: Type) -> Id (a :: Type) +Wrap :: + forall (t3 :: Type) (f :: (t3 :: Type) -> Type) (a :: (t3 :: Type)). + (f :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) -> + Wrap @(t3 :: Type) (f :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) +Compose :: + forall (t8 :: Type) (t7 :: Type) (f :: (t8 :: Type) -> Type) (g :: (t7 :: Type) -> (t8 :: Type)) + (a :: (t7 :: Type)). + (f :: (t8 :: Type) -> Type) ((g :: (t7 :: Type) -> (t8 :: Type)) (a :: (t7 :: Type))) -> + Compose @(t8 :: Type) @(t7 :: Type) + (f :: (t8 :: Type) -> Type) + (g :: (t7 :: Type) -> (t8 :: Type)) + (a :: (t7 :: Type)) +NoOrd :: forall (a :: Type). (a :: Type) -> NoOrd (a :: Type) + +Types +Id :: Type -> Type +Wrap :: forall (t3 :: Type). ((t3 :: Type) -> Type) -> (t3 :: Type) -> Type +Compose :: + forall (t8 :: Type) (t7 :: Type). + ((t8 :: Type) -> Type) -> ((t7 :: Type) -> (t8 :: Type)) -> (t7 :: Type) -> Type +NoOrd :: Type -> Type + +Roles +Id = [Representational] +Wrap = [Representational, Nominal] +Compose = [Representational, Representational, Nominal] +NoOrd = [Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(2), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(2), + }, + crumbs: [ + TermDeclaration( + Idx::(9), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(11), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(12), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(13), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(2), + }, + crumbs: [ + TermDeclaration( + Idx::(14), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(25), + class_id: Idx::(2), + }, + crumbs: [ + TermDeclaration( + Idx::(18), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index f17283e2..52b84308 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -205,3 +205,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_087_derive_ord_1_higher_kinded_main() { run_test("087_derive_ord_1_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_088_derive_eq_1_main() { run_test("088_derive_eq_1", "Main"); } + +#[rustfmt::skip] #[test] fn test_089_derive_ord_1_main() { run_test("089_derive_ord_1", "Main"); } From 2c965bd91688eae7e5af09ca95432e19f4d029c1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:26:10 +0800 Subject: [PATCH 312/386] Implement deriving for Ord1 --- compiler-core/checking2/src/source/derive.rs | 6 ++-- .../checking2/src/source/derive/eq1_ord1.rs | 18 +++++++++++ .../checking2/src/source/derive/head.rs | 7 ++++ .../checking2/089_derive_ord_1/Main.snap | 32 +++---------------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 6ef36af0..1f280ca1 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -27,6 +27,7 @@ enum DeriveDispatch { Eq, Eq1, Ord, + Ord1, SupportedButNotImplemented, Unsupported, } @@ -70,8 +71,9 @@ where DeriveDispatch::Eq1 } else if class == context.known_types.ord { DeriveDispatch::Ord - } else if class == context.known_types.ord1 - || class == context.known_types.functor + } else if class == context.known_types.ord1 { + DeriveDispatch::Ord1 + } else if class == context.known_types.functor || class == context.known_types.bifunctor || class == context.known_types.contravariant || class == context.known_types.profunctor diff --git a/compiler-core/checking2/src/source/derive/eq1_ord1.rs b/compiler-core/checking2/src/source/derive/eq1_ord1.rs index 6bef7d76..d93b036e 100644 --- a/compiler-core/checking2/src/source/derive/eq1_ord1.rs +++ b/compiler-core/checking2/src/source/derive/eq1_ord1.rs @@ -28,6 +28,24 @@ where check_derive_delegate_constraint(state, context, class_file, class_id, arguments, eq) } +pub(super) fn check_derive_ord1( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(ord) = context.known_types.ord else { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + return Ok(None); + }; + + check_derive_delegate_constraint(state, context, class_file, class_id, arguments, ord) +} + fn check_derive_delegate_constraint( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 46785468..f041944c 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -183,6 +183,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Ord1 => eq1_ord1::check_derive_ord1( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), DeriveDispatch::Unsupported => { state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); diff --git a/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap index b115c1b9..31243335 100644 --- a/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap +++ b/tests-integration/fixtures/checking2/089_derive_ord_1/Main.snap @@ -34,17 +34,6 @@ Compose = [Representational, Representational, Nominal] NoOrd = [Representational] Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(2), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} CheckError { kind: NoInstanceFound { constraint: Id(9), @@ -65,17 +54,6 @@ CheckError { ), ], } -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(2), - }, - crumbs: [ - TermDeclaration( - Idx::(9), - ), - ], -} CheckError { kind: NoInstanceFound { constraint: Id(11), @@ -107,9 +85,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(2), + kind: NoInstanceFound { + constraint: Id(14), }, crumbs: [ TermDeclaration( @@ -118,9 +95,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(25), - class_id: Idx::(2), + kind: NoInstanceFound { + constraint: Id(15), }, crumbs: [ TermDeclaration( From 4a77c605737dfbb401c1b32b6a7f1f2e3136ad6a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:27:33 +0800 Subject: [PATCH 313/386] Add smoke tests for Functor --- .../090_derive_functor_simple/Main.purs | 12 ++++ .../090_derive_functor_simple/Main.snap | 57 ++++++++++++++++ .../Main.purs | 12 ++++ .../Main.snap | 57 ++++++++++++++++ .../Main.purs | 9 +++ .../Main.snap | 68 +++++++++++++++++++ .../tests/checking2/generated.rs | 6 ++ 7 files changed, 221 insertions(+) create mode 100644 tests-integration/fixtures/checking2/090_derive_functor_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap create mode 100644 tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.purs create mode 100644 tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap diff --git a/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.purs b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.purs new file mode 100644 index 00000000..391e32cb --- /dev/null +++ b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Functor (class Functor) + +data Identity a = Identity a +derive instance Functor Identity + +data Const e a = Const e +derive instance Functor (Const e) + +data Maybe a = Nothing | Just a +derive instance Functor Maybe diff --git a/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap new file mode 100644 index 00000000..e21d576f --- /dev/null +++ b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) +Const :: + forall (t3 :: Type) (e :: Type) (a :: (t3 :: Type)). + (e :: Type) -> Const @(t3 :: Type) (e :: Type) (a :: (t3 :: Type)) +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) + +Types +Identity :: Type -> Type +Const :: forall (t3 :: Type). Type -> (t3 :: Type) -> Type +Maybe :: Type -> Type + +Roles +Identity = [Representational] +Const = [Representational, Phantom] +Maybe = [Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.purs b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.purs new file mode 100644 index 00000000..d35c7ea6 --- /dev/null +++ b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Functor (class Functor) + +data Predicate a = Predicate (a -> Boolean) +derive instance Functor Predicate + +data Reader r a = Reader (r -> a) +derive instance Functor (Reader r) + +data Cont r a = Cont ((a -> r) -> r) +derive instance Functor (Cont r) diff --git a/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap new file mode 100644 index 00000000..cbbbbfc4 --- /dev/null +++ b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Predicate :: forall (a :: Type). ((a :: Type) -> Boolean) -> Predicate (a :: Type) +Reader :: + forall (r :: Type) (a :: Type). ((r :: Type) -> (a :: Type)) -> Reader (r :: Type) (a :: Type) +Cont :: + forall (r :: Type) (a :: Type). + (((a :: Type) -> (r :: Type)) -> (r :: Type)) -> Cont (r :: Type) (a :: Type) + +Types +Predicate :: Type -> Type +Reader :: Type -> Type -> Type +Cont :: Type -> Type -> Type + +Roles +Predicate = [Representational] +Reader = [Representational, Representational] +Cont = [Representational, Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.purs b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.purs new file mode 100644 index 00000000..b5309b6b --- /dev/null +++ b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Functor (class Functor) + +data Pair a b = Pair a b +derive instance Functor (Pair Int String) + +data Unit = Unit +derive instance Functor Unit diff --git a/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap new file mode 100644 index 00000000..a8a0d896 --- /dev/null +++ b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap @@ -0,0 +1,68 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Pair :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Pair (a :: Type) (b :: Type) +Unit :: Unit + +Types +Pair :: Type -> Type -> Type +Unit :: Type + +Roles +Pair = [Representational, Representational] +Unit = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(6), + t2: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + CheckingKind( + AstId(19), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(6), + t2: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + CheckingKind( + AstId(28), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(22), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 52b84308..975d4348 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -207,3 +207,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_088_derive_eq_1_main() { run_test("088_derive_eq_1", "Main"); } #[rustfmt::skip] #[test] fn test_089_derive_ord_1_main() { run_test("089_derive_ord_1", "Main"); } + +#[rustfmt::skip] #[test] fn test_090_derive_functor_simple_main() { run_test("090_derive_functor_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_091_derive_functor_contravariant_error_main() { run_test("091_derive_functor_contravariant_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_092_derive_functor_insufficient_params_main() { run_test("092_derive_functor_insufficient_params", "Main"); } From a496946c645b4288a80543b9255ee74e015a957e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:28:11 +0800 Subject: [PATCH 314/386] Implement deriving for Functor --- compiler-core/checking2/src/source/derive.rs | 13 +- .../checking2/src/source/derive/functor.rs | 49 ++++ .../checking2/src/source/derive/head.rs | 11 +- .../checking2/src/source/derive/member.rs | 20 +- .../checking2/src/source/derive/variance.rs | 276 ++++++++++++++++++ .../090_derive_functor_simple/Main.snap | 35 --- .../Main.snap | 27 +- .../Main.snap | 10 +- 8 files changed, 371 insertions(+), 70 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 1f280ca1..1cff48cf 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -21,11 +21,13 @@ use crate::core::TypeId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::state::CheckState; +use crate::source::derive::variance::VarianceConfig; #[derive(Clone, Copy)] enum DeriveDispatch { Eq, Eq1, + Functor, Ord, Ord1, SupportedButNotImplemented, @@ -44,6 +46,12 @@ pub(super) enum DeriveStrategy { derived_type: TypeId, class: (FileId, TypeItemId), }, + VarianceConstraints { + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + config: VarianceConfig, + }, Unsupported, } @@ -69,12 +77,13 @@ where DeriveDispatch::Eq } else if class == context.known_types.eq1 { DeriveDispatch::Eq1 + } else if class == context.known_types.functor { + DeriveDispatch::Functor } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.functor - || class == context.known_types.bifunctor + } else if class == context.known_types.bifunctor || class == context.known_types.contravariant || class == context.known_types.profunctor || class == context.known_types.foldable diff --git a/compiler-core/checking2/src/source/derive/functor.rs b/compiler-core/checking2/src/source/derive/functor.rs index 8b137891..2e1789bb 100644 --- a/compiler-core/checking2/src/source/derive/functor.rs +++ b/compiler-core/checking2/src/source/derive/functor.rs @@ -1 +1,50 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; +use super::variance::{Variance, VarianceConfig}; + +pub(super) fn check_derive_functor( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let functor = Some((class_file, class_id)); + let config = VarianceConfig::Single((Variance::Covariant, functor)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index f041944c..037205ec 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -10,7 +10,9 @@ use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::types; use crate::state::CheckState; -use super::{DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq1_ord1, eq_ord}; +use super::{ + DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq1_ord1, eq_ord, functor, +}; pub fn check_derive_declarations( state: &mut CheckState, @@ -176,6 +178,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Functor => functor::check_derive_functor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index 14cda2c2..e0e661a4 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -6,7 +6,7 @@ use crate::core::Type; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; -use super::{DeriveHeadResult, DeriveStrategy, field, tools}; +use super::{DeriveHeadResult, DeriveStrategy, field, tools, variance}; pub fn check_derive_members( state: &mut CheckState, @@ -66,6 +66,24 @@ where generate_delegate_constraint(state, context, derived_type, class); tools::solve_and_report_constraints(state, context)?; } + DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config } => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + variance::generate_variance_constraints( + state, + context, + data_file, + data_id, + derived_type, + config, + )?; + tools::solve_and_report_constraints(state, context)?; + } DeriveStrategy::Unsupported => { state.insert_error(ErrorKind::DeriveNotSupportedYet { class_file: result.class_file, diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 8b137891..3fcdb3a4 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -1 +1,277 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::substitute::SubstituteName; +use crate::core::{Name, Type, TypeId, normalise, toolkit}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::tools; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(in crate::source) enum Variance { + Covariant, + Contravariant, +} + +impl Variance { + fn flip(self) -> Variance { + match self { + Variance::Covariant => Variance::Contravariant, + Variance::Contravariant => Variance::Covariant, + } + } +} + +type ParameterConfig = (Variance, Option<(FileId, TypeItemId)>); + +#[derive(Clone, Copy)] +pub(in crate::source) enum VarianceConfig { + Single(ParameterConfig), + Pair(ParameterConfig, ParameterConfig), +} + +struct DerivedParameter { + name: Name, + expected: Variance, + class: Option<(FileId, TypeItemId)>, +} + +enum DerivedRigids { + Invalid, + Single(DerivedParameter), + Pair(DerivedParameter, DerivedParameter), +} + +impl DerivedRigids { + fn get(&self, name: Name) -> Option<&DerivedParameter> { + self.iter().find(|parameter| parameter.name == name) + } + + fn iter(&self) -> impl Iterator { + let (first, second) = match self { + DerivedRigids::Invalid => (None, None), + DerivedRigids::Single(first) => (Some(first), None), + DerivedRigids::Pair(first, second) => (Some(first), Some(second)), + }; + first.into_iter().chain(second) + } +} + +pub(super) fn generate_variance_constraints( + state: &mut CheckState, + context: &CheckContext, + data_file: FileId, + data_id: TypeItemId, + derived_type: TypeId, + config: VarianceConfig, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + for constructor_id in tools::lookup_data_constructors(context, data_file, data_id)? { + let constructor_t = toolkit::lookup_file_term(state, context, data_file, constructor_id)?; + let (fields, rigids) = + extract_fields_with_rigids(state, context, constructor_t, derived_type, config)?; + + for field in fields { + check_variance_field(state, context, field, Variance::Covariant, &rigids)?; + } + } + + Ok(()) +} + +fn extract_fields_with_rigids( + state: &mut CheckState, + context: &CheckContext, + constructor_t: TypeId, + derived_type: TypeId, + config: VarianceConfig, +) -> QueryResult<(Vec, DerivedRigids)> +where + Q: ExternalQueries, +{ + let (_, arguments) = toolkit::extract_type_application(state, context, derived_type)?; + let mut arguments = arguments.iter().copied(); + let mut current = constructor_t; + let mut names = vec![]; + + loop { + current = normalise::normalise(state, context, current)?; + let Type::Forall(binder_id, inner) = context.lookup_type(current) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let replacement = arguments.next().unwrap_or_else(|| { + let rigid = state.fresh_rigid(context.queries, binder.kind); + let Type::Rigid(name, _, _) = context.lookup_type(rigid) else { + unreachable!("fresh_rigid must create Type::Rigid") + }; + names.push(name); + rigid + }); + + current = SubstituteName::one(state, context, binder.name, replacement, inner)?; + } + + let rigids = match (config, &names[..]) { + (VarianceConfig::Single((expected, class)), [.., a]) => { + DerivedRigids::Single(DerivedParameter { name: *a, expected, class }) + } + (VarianceConfig::Pair((first_expected, first_class), (second_expected, second_class)), [.., a, b]) => { + DerivedRigids::Pair( + DerivedParameter { name: *a, expected: first_expected, class: first_class }, + DerivedParameter { name: *b, expected: second_expected, class: second_class }, + ) + } + _ => { + let type_message = state.pretty_id(context, derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + DerivedRigids::Invalid + } + }; + + let toolkit::InspectFunction { arguments: fields, .. } = + toolkit::inspect_function(state, context, current)?; + + Ok((fields, rigids)) +} + +fn check_variance_field( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, + variance: Variance, + rigids: &DerivedRigids, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let type_id = normalise::normalise(state, context, type_id)?; + + match context.lookup_type(type_id) { + Type::Rigid(name, _, _) => { + if let Some(parameter) = rigids.get(name) { + emit_variance_error(state, context, type_id, variance, parameter.expected)?; + } + } + Type::Function(argument, result) => { + check_variance_field(state, context, argument, variance.flip(), rigids)?; + check_variance_field(state, context, result, variance, rigids)?; + } + Type::Application(function, argument) => { + let function = normalise::normalise(state, context, function)?; + if function == context.prim.record { + check_variance_field(state, context, argument, variance, rigids)?; + } else { + for parameter in rigids.iter() { + if contains_rigid_name(state, context, argument, parameter.name)? { + emit_variance_error(state, context, type_id, variance, parameter.expected)?; + if variance == parameter.expected { + if let Some(class) = parameter.class { + tools::emit_constraint(context, state, class, function); + } else { + state.insert_error(ErrorKind::DeriveMissingFunctor); + } + } + } + } + check_variance_field(state, context, argument, variance, rigids)?; + } + } + Type::KindApplication(_, argument) => { + check_variance_field(state, context, argument, variance, rigids)?; + } + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + for field in row.fields.iter() { + check_variance_field(state, context, field.id, variance, rigids)?; + } + if let Some(tail) = row.tail { + check_variance_field(state, context, tail, variance, rigids)?; + } + } + _ => {} + } + + Ok(()) +} + +fn emit_variance_error( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, + actual: Variance, + expected: Variance, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + if actual == expected { + return Ok(()); + } + + let type_message = state.pretty_id(context, type_id)?; + match actual { + Variance::Covariant => state.insert_error(ErrorKind::CovariantOccurrence { type_message }), + Variance::Contravariant => { + state.insert_error(ErrorKind::ContravariantOccurrence { type_message }) + } + } + Ok(()) +} + +fn contains_rigid_name( + state: &mut CheckState, + context: &CheckContext, + type_id: TypeId, + name: Name, +) -> QueryResult +where + Q: ExternalQueries, +{ + let type_id = normalise::normalise(state, context, type_id)?; + Ok(match context.lookup_type(type_id) { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + contains_rigid_name(state, context, function, name)? + || contains_rigid_name(state, context, argument, name)? + } + Type::OperatorApplication(_, _, left, right) => { + contains_rigid_name(state, context, left, name)? + || contains_rigid_name(state, context, right, name)? + } + Type::SynonymApplication(synonym_id) => { + let synonym = context.lookup_synonym(synonym_id); + let mut contains = false; + for &argument in synonym.arguments.iter() { + contains |= contains_rigid_name(state, context, argument, name)?; + } + contains + } + Type::Forall(_, inner) | Type::Constrained(_, inner) | Type::Kinded(inner, _) => { + contains_rigid_name(state, context, inner, name)? + } + Type::Function(argument, result) => { + contains_rigid_name(state, context, argument, name)? + || contains_rigid_name(state, context, result, name)? + } + Type::Row(row_id) => { + let row = context.lookup_row_type(row_id); + let mut contains = false; + for field in row.fields.iter() { + contains |= contains_rigid_name(state, context, field.id, name)?; + } + if let Some(tail) = row.tail { + contains |= contains_rigid_name(state, context, tail, name)?; + } + contains + } + Type::Rigid(rigid_name, _, _) => rigid_name == name, + _ => false, + }) +} diff --git a/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap index e21d576f..6c077c2b 100644 --- a/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap +++ b/tests-integration/fixtures/checking2/090_derive_functor_simple/Main.snap @@ -20,38 +20,3 @@ Roles Identity = [Representational] Const = [Representational, Phantom] Maybe = [Representational] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap index cbbbbfc4..1f606cb8 100644 --- a/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking2/091_derive_functor_contravariant_error/Main.snap @@ -23,9 +23,8 @@ Cont = [Representational, Representational] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), + kind: ContravariantOccurrence { + type_message: Id(7), }, crumbs: [ TermDeclaration( @@ -33,25 +32,3 @@ CheckError { ), ], } -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap index a8a0d896..1806a00e 100644 --- a/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking2/092_derive_functor_insufficient_params/Main.snap @@ -45,9 +45,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), + kind: CannotDeriveForType { + type_message: Id(8), }, crumbs: [ TermDeclaration( @@ -56,9 +55,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(22), - class_id: Idx::(0), + kind: CannotDeriveForType { + type_message: Id(9), }, crumbs: [ TermDeclaration( From 54be30b401e956803f23d0957e4e1687df4a4fa7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:31:16 +0800 Subject: [PATCH 315/386] Add smoke tests for Bifunctor --- .../093_derive_bifunctor_simple/Main.purs | 12 ++++ .../093_derive_bifunctor_simple/Main.snap | 58 +++++++++++++++++++ .../Main.purs | 6 ++ .../Main.snap | 37 ++++++++++++ .../Main.purs | 6 ++ .../Main.snap | 42 ++++++++++++++ .../tests/checking2/generated.rs | 6 ++ 7 files changed, 167 insertions(+) create mode 100644 tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.purs create mode 100644 tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap create mode 100644 tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.purs create mode 100644 tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap diff --git a/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.purs b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.purs new file mode 100644 index 00000000..aa956e4b --- /dev/null +++ b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Bifunctor (class Bifunctor) + +data Either a b = Left a | Right b +derive instance Bifunctor Either + +data Pair a b = Pair a b +derive instance Bifunctor Pair + +data Const2 e a b = Const2 e +derive instance Bifunctor (Const2 e) diff --git a/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap new file mode 100644 index 00000000..4934d92a --- /dev/null +++ b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap @@ -0,0 +1,58 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +Pair :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Pair (a :: Type) (b :: Type) +Const2 :: + forall (t8 :: Type) (t7 :: Type) (e :: Type) (a :: (t8 :: Type)) (b :: (t7 :: Type)). + (e :: Type) -> + Const2 @(t8 :: Type) @(t7 :: Type) (e :: Type) (a :: (t8 :: Type)) (b :: (t7 :: Type)) + +Types +Either :: Type -> Type -> Type +Pair :: Type -> Type -> Type +Const2 :: forall (t8 :: Type) (t7 :: Type). Type -> (t8 :: Type) -> (t7 :: Type) -> Type + +Roles +Either = [Representational, Representational] +Pair = [Representational, Representational] +Const2 = [Representational, Phantom, Phantom] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(16), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(16), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(16), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.purs b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.purs new file mode 100644 index 00000000..2a0fb76e --- /dev/null +++ b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.purs @@ -0,0 +1,6 @@ +module Main where + +import Data.Bifunctor (class Bifunctor) + +data WrapBoth f g a b = WrapBoth (f a) (g b) +derive instance Bifunctor (WrapBoth f g) diff --git a/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap new file mode 100644 index 00000000..6d85703b --- /dev/null +++ b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +WrapBoth :: + forall (t5 :: Type) (t4 :: Type) (f :: (t5 :: Type) -> Type) (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) (b :: (t4 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + (g :: (t4 :: Type) -> Type) (b :: (t4 :: Type)) -> + WrapBoth @(t5 :: Type) @(t4 :: Type) + (f :: (t5 :: Type) -> Type) + (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) + (b :: (t4 :: Type)) + +Types +WrapBoth :: + forall (t5 :: Type) (t4 :: Type). + ((t5 :: Type) -> Type) -> ((t4 :: Type) -> Type) -> (t5 :: Type) -> (t4 :: Type) -> Type + +Roles +WrapBoth = [Representational, Representational, Nominal, Nominal] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(16), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.purs b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.purs new file mode 100644 index 00000000..9a1af2ee --- /dev/null +++ b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.purs @@ -0,0 +1,6 @@ +module Main where + +import Data.Bifunctor (class Bifunctor) + +data Triple a b c = Triple a b c +derive instance Bifunctor (Triple Int String) diff --git a/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap new file mode 100644 index 00000000..287a4cde --- /dev/null +++ b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap @@ -0,0 +1,42 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Triple :: + forall (a :: Type) (b :: Type) (c :: Type). + (a :: Type) -> (b :: Type) -> (c :: Type) -> Triple (a :: Type) (b :: Type) (c :: Type) + +Types +Triple :: Type -> Type -> Type -> Type + +Roles +Triple = [Representational, Representational, Representational] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + CheckingKind( + AstId(21), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(16), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 975d4348..3396db85 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -213,3 +213,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_091_derive_functor_contravariant_error_main() { run_test("091_derive_functor_contravariant_error", "Main"); } #[rustfmt::skip] #[test] fn test_092_derive_functor_insufficient_params_main() { run_test("092_derive_functor_insufficient_params", "Main"); } + +#[rustfmt::skip] #[test] fn test_093_derive_bifunctor_simple_main() { run_test("093_derive_bifunctor_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_094_derive_bifunctor_missing_functor_main() { run_test("094_derive_bifunctor_missing_functor", "Main"); } + +#[rustfmt::skip] #[test] fn test_095_derive_bifunctor_insufficient_params_main() { run_test("095_derive_bifunctor_insufficient_params", "Main"); } From bd060e645a28a9d92a7b3069a815a6bbdd9351cb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:31:43 +0800 Subject: [PATCH 316/386] Implement deriving for Bifunctor --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../checking2/src/source/derive/functor.rs | 38 +++++++++++++++++++ .../checking2/src/source/derive/head.rs | 7 ++++ .../093_derive_bifunctor_simple/Main.snap | 35 ----------------- .../Main.snap | 15 ++++++-- .../Main.snap | 5 +-- 6 files changed, 63 insertions(+), 43 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 1cff48cf..baccfde8 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -28,6 +28,7 @@ enum DeriveDispatch { Eq, Eq1, Functor, + Bifunctor, Ord, Ord1, SupportedButNotImplemented, @@ -79,12 +80,13 @@ where DeriveDispatch::Eq1 } else if class == context.known_types.functor { DeriveDispatch::Functor + } else if class == context.known_types.bifunctor { + DeriveDispatch::Bifunctor } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.bifunctor - || class == context.known_types.contravariant + } else if class == context.known_types.contravariant || class == context.known_types.profunctor || class == context.known_types.foldable || class == context.known_types.bifoldable diff --git a/compiler-core/checking2/src/source/derive/functor.rs b/compiler-core/checking2/src/source/derive/functor.rs index 2e1789bb..afec174f 100644 --- a/compiler-core/checking2/src/source/derive/functor.rs +++ b/compiler-core/checking2/src/source/derive/functor.rs @@ -48,3 +48,41 @@ where config, })) } + +pub(super) fn check_derive_bifunctor( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let wrapper = context.known_types.functor; + let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 037205ec..7d6a0cad 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -185,6 +185,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Bifunctor => functor::check_derive_bifunctor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap index 4934d92a..0f50c456 100644 --- a/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap +++ b/tests-integration/fixtures/checking2/093_derive_bifunctor_simple/Main.snap @@ -21,38 +21,3 @@ Roles Either = [Representational, Representational] Pair = [Representational, Representational] Const2 = [Representational, Phantom, Phantom] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(16), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(16), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(16), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap index 6d85703b..111f20bc 100644 --- a/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap +++ b/tests-integration/fixtures/checking2/094_derive_bifunctor_missing_functor/Main.snap @@ -25,9 +25,18 @@ WrapBoth = [Representational, Representational, Nominal, Nominal] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(16), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), }, crumbs: [ TermDeclaration( diff --git a/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap index 287a4cde..4c0226ae 100644 --- a/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap +++ b/tests-integration/fixtures/checking2/095_derive_bifunctor_insufficient_params/Main.snap @@ -30,9 +30,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(16), - class_id: Idx::(0), + kind: CannotDeriveForType { + type_message: Id(10), }, crumbs: [ TermDeclaration( From fab01fd317483c202d51f8a1fbb3ba4608433ead Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:32:56 +0800 Subject: [PATCH 317/386] Add smoke tests for Contravariant --- .../096_derive_contravariant_simple/Main.purs | 12 +++++ .../096_derive_contravariant_simple/Main.snap | 54 +++++++++++++++++++ .../097_derive_contravariant_error/Main.purs | 9 ++++ .../097_derive_contravariant_error/Main.snap | 40 ++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 119 insertions(+) create mode 100644 tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap diff --git a/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.purs b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.purs new file mode 100644 index 00000000..d6c05c84 --- /dev/null +++ b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Functor.Contravariant (class Contravariant) + +data Predicate a = Predicate (a -> Boolean) +derive instance Contravariant Predicate + +data Comparison a = Comparison (a -> a -> Boolean) +derive instance Contravariant Comparison + +data Op a b = Op (b -> a) +derive instance Contravariant (Op a) diff --git a/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap new file mode 100644 index 00000000..c9c6c928 --- /dev/null +++ b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap @@ -0,0 +1,54 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Predicate :: forall (a :: Type). ((a :: Type) -> Boolean) -> Predicate (a :: Type) +Comparison :: forall (a :: Type). ((a :: Type) -> (a :: Type) -> Boolean) -> Comparison (a :: Type) +Op :: forall (a :: Type) (b :: Type). ((b :: Type) -> (a :: Type)) -> Op (a :: Type) (b :: Type) + +Types +Predicate :: Type -> Type +Comparison :: Type -> Type +Op :: Type -> Type -> Type + +Roles +Predicate = [Representational] +Comparison = [Representational] +Op = [Representational, Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(21), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(21), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(21), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.purs b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.purs new file mode 100644 index 00000000..0713c15e --- /dev/null +++ b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Functor.Contravariant (class Contravariant) + +data Identity a = Identity a +derive instance Contravariant Identity + +data Producer a = Producer (Int -> a) +derive instance Contravariant Producer diff --git a/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap new file mode 100644 index 00000000..217c787c --- /dev/null +++ b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) +Producer :: forall (a :: Type). (Int -> (a :: Type)) -> Producer (a :: Type) + +Types +Identity :: Type -> Type +Producer :: Type -> Type + +Roles +Identity = [Representational] +Producer = [Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(21), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(21), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 3396db85..8d655942 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -219,3 +219,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_094_derive_bifunctor_missing_functor_main() { run_test("094_derive_bifunctor_missing_functor", "Main"); } #[rustfmt::skip] #[test] fn test_095_derive_bifunctor_insufficient_params_main() { run_test("095_derive_bifunctor_insufficient_params", "Main"); } + +#[rustfmt::skip] #[test] fn test_096_derive_contravariant_simple_main() { run_test("096_derive_contravariant_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_097_derive_contravariant_error_main() { run_test("097_derive_contravariant_error", "Main"); } From 46e8a68c24f4ebea082d47e919b781f2bb53621e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:33:26 +0800 Subject: [PATCH 318/386] Implement deriving for Contravariant --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../src/source/derive/contravariant.rs | 49 +++++++++++++++++++ .../checking2/src/source/derive/head.rs | 10 +++- .../096_derive_contravariant_simple/Main.snap | 35 ------------- .../097_derive_contravariant_error/Main.snap | 10 ++-- 5 files changed, 66 insertions(+), 44 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index baccfde8..6fdbc06f 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -29,6 +29,7 @@ enum DeriveDispatch { Eq1, Functor, Bifunctor, + Contravariant, Ord, Ord1, SupportedButNotImplemented, @@ -82,12 +83,13 @@ where DeriveDispatch::Functor } else if class == context.known_types.bifunctor { DeriveDispatch::Bifunctor + } else if class == context.known_types.contravariant { + DeriveDispatch::Contravariant } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.contravariant - || class == context.known_types.profunctor + } else if class == context.known_types.profunctor || class == context.known_types.foldable || class == context.known_types.bifoldable || class == context.known_types.traversable diff --git a/compiler-core/checking2/src/source/derive/contravariant.rs b/compiler-core/checking2/src/source/derive/contravariant.rs index 8b137891..1bd50fb0 100644 --- a/compiler-core/checking2/src/source/derive/contravariant.rs +++ b/compiler-core/checking2/src/source/derive/contravariant.rs @@ -1 +1,50 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; +use super::variance::{Variance, VarianceConfig}; + +pub(super) fn check_derive_contravariant( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let contravariant = Some((class_file, class_id)); + let config = VarianceConfig::Single((Variance::Contravariant, contravariant)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 7d6a0cad..510dd0fd 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -11,7 +11,8 @@ use crate::source::types; use crate::state::CheckState; use super::{ - DeriveDispatch, DeriveHeadResult, DeriveStrategy, derive_dispatch, eq1_ord1, eq_ord, functor, + DeriveDispatch, DeriveHeadResult, DeriveStrategy, contravariant, derive_dispatch, eq1_ord1, + eq_ord, functor, }; pub fn check_derive_declarations( @@ -192,6 +193,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Contravariant => contravariant::check_derive_contravariant( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap index c9c6c928..552e792f 100644 --- a/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap +++ b/tests-integration/fixtures/checking2/096_derive_contravariant_simple/Main.snap @@ -17,38 +17,3 @@ Roles Predicate = [Representational] Comparison = [Representational] Op = [Representational, Representational] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(21), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(21), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(21), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap index 217c787c..11ea5a82 100644 --- a/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap +++ b/tests-integration/fixtures/checking2/097_derive_contravariant_error/Main.snap @@ -17,9 +17,8 @@ Producer = [Representational] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(21), - class_id: Idx::(0), + kind: CovariantOccurrence { + type_message: Id(6), }, crumbs: [ TermDeclaration( @@ -28,9 +27,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(21), - class_id: Idx::(0), + kind: CovariantOccurrence { + type_message: Id(7), }, crumbs: [ TermDeclaration( From 9d2c931b78779eab7bcd71980a36b1d28394bbba Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:34:13 +0800 Subject: [PATCH 319/386] Add smoke tests for Profunctor --- .../098_derive_profunctor_simple/Main.purs | 12 ++++ .../098_derive_profunctor_simple/Main.snap | 57 +++++++++++++++++++ .../099_derive_profunctor_error/Main.purs | 9 +++ .../099_derive_profunctor_error/Main.snap | 43 ++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 125 insertions(+) create mode 100644 tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap diff --git a/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.purs b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.purs new file mode 100644 index 00000000..4e9b3ce4 --- /dev/null +++ b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Profunctor (class Profunctor) + +data Fn a b = Fn (a -> b) +derive instance Profunctor Fn + +data ConstR r a b = ConstR (a -> r) +derive instance Profunctor (ConstR r) + +data Choice a b = GoLeft (a -> Int) | GoRight b +derive instance Profunctor Choice diff --git a/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap new file mode 100644 index 00000000..88df1aca --- /dev/null +++ b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Fn :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> Fn (a :: Type) (b :: Type) +ConstR :: + forall (t5 :: Type) (r :: Type) (a :: Type) (b :: (t5 :: Type)). + ((a :: Type) -> (r :: Type)) -> ConstR @(t5 :: Type) (r :: Type) (a :: Type) (b :: (t5 :: Type)) +GoLeft :: forall (a :: Type) (b :: Type). ((a :: Type) -> Int) -> Choice (a :: Type) (b :: Type) +GoRight :: forall (a :: Type) (b :: Type). (b :: Type) -> Choice (a :: Type) (b :: Type) + +Types +Fn :: Type -> Type -> Type +ConstR :: forall (t5 :: Type). Type -> Type -> (t5 :: Type) -> Type +Choice :: Type -> Type -> Type + +Roles +Fn = [Representational, Representational] +ConstR = [Representational, Representational, Phantom] +Choice = [Representational, Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(27), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(27), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(27), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.purs b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.purs new file mode 100644 index 00000000..d4aa9394 --- /dev/null +++ b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Profunctor (class Profunctor) + +data WrongFirst a b = WrongFirst a b +derive instance Profunctor WrongFirst + +data WrongSecond a b = WrongSecond (b -> a) +derive instance Profunctor WrongSecond diff --git a/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap new file mode 100644 index 00000000..49fdf0ab --- /dev/null +++ b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap @@ -0,0 +1,43 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +WrongFirst :: + forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> WrongFirst (a :: Type) (b :: Type) +WrongSecond :: + forall (a :: Type) (b :: Type). + ((b :: Type) -> (a :: Type)) -> WrongSecond (a :: Type) (b :: Type) + +Types +WrongFirst :: Type -> Type -> Type +WrongSecond :: Type -> Type -> Type + +Roles +WrongFirst = [Representational, Representational] +WrongSecond = [Representational, Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(27), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(27), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 8d655942..3c2a6d42 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -223,3 +223,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_096_derive_contravariant_simple_main() { run_test("096_derive_contravariant_simple", "Main"); } #[rustfmt::skip] #[test] fn test_097_derive_contravariant_error_main() { run_test("097_derive_contravariant_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_098_derive_profunctor_simple_main() { run_test("098_derive_profunctor_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_099_derive_profunctor_error_main() { run_test("099_derive_profunctor_error", "Main"); } From a63cd11ae33b8b56cf8e84374b485761e5adeb29 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:34:41 +0800 Subject: [PATCH 320/386] Implement deriving for Profunctor --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../src/source/derive/contravariant.rs | 40 +++++++++++++++++++ .../checking2/src/source/derive/head.rs | 7 ++++ .../098_derive_profunctor_simple/Main.snap | 35 ---------------- .../099_derive_profunctor_error/Main.snap | 20 +++++++--- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 6fdbc06f..b7c98be1 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -30,6 +30,7 @@ enum DeriveDispatch { Functor, Bifunctor, Contravariant, + Profunctor, Ord, Ord1, SupportedButNotImplemented, @@ -85,12 +86,13 @@ where DeriveDispatch::Bifunctor } else if class == context.known_types.contravariant { DeriveDispatch::Contravariant + } else if class == context.known_types.profunctor { + DeriveDispatch::Profunctor } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.profunctor - || class == context.known_types.foldable + } else if class == context.known_types.foldable || class == context.known_types.bifoldable || class == context.known_types.traversable || class == context.known_types.bitraversable diff --git a/compiler-core/checking2/src/source/derive/contravariant.rs b/compiler-core/checking2/src/source/derive/contravariant.rs index 1bd50fb0..502e80ef 100644 --- a/compiler-core/checking2/src/source/derive/contravariant.rs +++ b/compiler-core/checking2/src/source/derive/contravariant.rs @@ -48,3 +48,43 @@ where config, })) } + +pub(super) fn check_derive_profunctor( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let contravariant = context.known_types.contravariant; + let functor = context.known_types.functor; + let config = + VarianceConfig::Pair((Variance::Contravariant, contravariant), (Variance::Covariant, functor)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 510dd0fd..2fb64b32 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -200,6 +200,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Profunctor => contravariant::check_derive_profunctor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap index 88df1aca..0047bce6 100644 --- a/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap +++ b/tests-integration/fixtures/checking2/098_derive_profunctor_simple/Main.snap @@ -20,38 +20,3 @@ Roles Fn = [Representational, Representational] ConstR = [Representational, Representational, Phantom] Choice = [Representational, Representational] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(27), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(27), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(27), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap index 49fdf0ab..c8ff77b3 100644 --- a/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap +++ b/tests-integration/fixtures/checking2/099_derive_profunctor_error/Main.snap @@ -20,9 +20,8 @@ WrongSecond = [Representational, Representational] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(27), - class_id: Idx::(0), + kind: CovariantOccurrence { + type_message: Id(8), }, crumbs: [ TermDeclaration( @@ -31,9 +30,18 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(27), - class_id: Idx::(0), + kind: ContravariantOccurrence { + type_message: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: CovariantOccurrence { + type_message: Id(10), }, crumbs: [ TermDeclaration( From 058b69293e16e691608c6445fbf73f282aa0a6bc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:36:03 +0800 Subject: [PATCH 321/386] Add smoke tests for Foldable --- .../100_derive_foldable_simple/Main.purs | 12 ++++ .../100_derive_foldable_simple/Main.snap | 57 +++++++++++++++++++ .../Main.purs | 9 +++ .../Main.snap | 46 +++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 128 insertions(+) create mode 100644 tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap diff --git a/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.purs b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.purs new file mode 100644 index 00000000..3b377612 --- /dev/null +++ b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Foldable (class Foldable) + +data Identity a = Identity a +derive instance Foldable Identity + +data Maybe a = Nothing | Just a +derive instance Foldable Maybe + +data Const e a = Const e +derive instance Foldable (Const e) diff --git a/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap new file mode 100644 index 00000000..1b8e4fa5 --- /dev/null +++ b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Const :: + forall (t4 :: Type) (e :: Type) (a :: (t4 :: Type)). + (e :: Type) -> Const @(t4 :: Type) (e :: Type) (a :: (t4 :: Type)) + +Types +Identity :: Type -> Type +Maybe :: Type -> Type +Const :: forall (t4 :: Type). Type -> (t4 :: Type) -> Type + +Roles +Identity = [Representational] +Maybe = [Representational] +Const = [Representational, Phantom] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(20), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(20), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(20), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.purs new file mode 100644 index 00000000..259227a9 --- /dev/null +++ b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Foldable (class Foldable) + +data Wrap f a = Wrap (f a) +derive instance Foldable f => Foldable (Wrap f) + +data WrapNoFoldable f a = WrapNoFoldable (f a) +derive instance Foldable (WrapNoFoldable f) diff --git a/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap new file mode 100644 index 00000000..4b006767 --- /dev/null +++ b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap @@ -0,0 +1,46 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrap :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + Wrap @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) +WrapNoFoldable :: + forall (t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + WrapNoFoldable @(t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) + +Types +Wrap :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type +WrapNoFoldable :: forall (t5 :: Type). ((t5 :: Type) -> Type) -> (t5 :: Type) -> Type + +Roles +Wrap = [Representational, Nominal] +WrapNoFoldable = [Representational, Nominal] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(20), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(20), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 3c2a6d42..d6f07e92 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -227,3 +227,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_098_derive_profunctor_simple_main() { run_test("098_derive_profunctor_simple", "Main"); } #[rustfmt::skip] #[test] fn test_099_derive_profunctor_error_main() { run_test("099_derive_profunctor_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_100_derive_foldable_simple_main() { run_test("100_derive_foldable_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_101_derive_foldable_higher_kinded_main() { run_test("101_derive_foldable_higher_kinded", "Main"); } From 0b23c4be79b30d7495541dedcd0de073455efa61 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:36:34 +0800 Subject: [PATCH 322/386] Implement deriving for Foldable --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../checking2/src/source/derive/foldable.rs | 49 +++++++++++++++++++ .../checking2/src/source/derive/head.rs | 9 +++- .../100_derive_foldable_simple/Main.snap | 35 ------------- .../Main.snap | 10 ++-- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index b7c98be1..317abc69 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -31,6 +31,7 @@ enum DeriveDispatch { Bifunctor, Contravariant, Profunctor, + Foldable, Ord, Ord1, SupportedButNotImplemented, @@ -88,12 +89,13 @@ where DeriveDispatch::Contravariant } else if class == context.known_types.profunctor { DeriveDispatch::Profunctor + } else if class == context.known_types.foldable { + DeriveDispatch::Foldable } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.foldable - || class == context.known_types.bifoldable + } else if class == context.known_types.bifoldable || class == context.known_types.traversable || class == context.known_types.bitraversable || class == context.known_types.newtype diff --git a/compiler-core/checking2/src/source/derive/foldable.rs b/compiler-core/checking2/src/source/derive/foldable.rs index 8b137891..7c8c6dc3 100644 --- a/compiler-core/checking2/src/source/derive/foldable.rs +++ b/compiler-core/checking2/src/source/derive/foldable.rs @@ -1 +1,50 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; +use super::variance::{Variance, VarianceConfig}; + +pub(super) fn check_derive_foldable( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let foldable = Some((class_file, class_id)); + let config = VarianceConfig::Single((Variance::Covariant, foldable)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 2fb64b32..eba21349 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -12,7 +12,7 @@ use crate::state::CheckState; use super::{ DeriveDispatch, DeriveHeadResult, DeriveStrategy, contravariant, derive_dispatch, eq1_ord1, - eq_ord, functor, + eq_ord, foldable, functor, }; pub fn check_derive_declarations( @@ -207,6 +207,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Foldable => foldable::check_derive_foldable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap index 1b8e4fa5..873cf2b9 100644 --- a/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap +++ b/tests-integration/fixtures/checking2/100_derive_foldable_simple/Main.snap @@ -20,38 +20,3 @@ Roles Identity = [Representational] Maybe = [Representational] Const = [Representational, Phantom] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(20), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(20), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(20), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap index 4b006767..fe316ede 100644 --- a/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/101_derive_foldable_higher_kinded/Main.snap @@ -23,9 +23,8 @@ WrapNoFoldable = [Representational, Nominal] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(20), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(6), }, crumbs: [ TermDeclaration( @@ -34,9 +33,8 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(20), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(7), }, crumbs: [ TermDeclaration( From eca6b07bb1f0ad34ac925e44c6fd2b4cb25c2104 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:37:33 +0800 Subject: [PATCH 323/386] Add smoke tests for Bifoldable --- .../102_derive_bifoldable_simple/Main.purs | 12 ++++ .../102_derive_bifoldable_simple/Main.snap | 58 +++++++++++++++++ .../Main.purs | 10 +++ .../Main.snap | 62 +++++++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 146 insertions(+) create mode 100644 tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap diff --git a/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.purs b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.purs new file mode 100644 index 00000000..f7557039 --- /dev/null +++ b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Bifoldable (class Bifoldable) + +data Either a b = Left a | Right b +derive instance Bifoldable Either + +data Pair a b = Pair a b +derive instance Bifoldable Pair + +data Const2 e a b = Const2 e +derive instance Bifoldable (Const2 e) diff --git a/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap new file mode 100644 index 00000000..823c09ca --- /dev/null +++ b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap @@ -0,0 +1,58 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +Pair :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Pair (a :: Type) (b :: Type) +Const2 :: + forall (t8 :: Type) (t7 :: Type) (e :: Type) (a :: (t8 :: Type)) (b :: (t7 :: Type)). + (e :: Type) -> + Const2 @(t8 :: Type) @(t7 :: Type) (e :: Type) (a :: (t8 :: Type)) (b :: (t7 :: Type)) + +Types +Either :: Type -> Type -> Type +Pair :: Type -> Type -> Type +Const2 :: forall (t8 :: Type) (t7 :: Type). Type -> (t8 :: Type) -> (t7 :: Type) -> Type + +Roles +Either = [Representational, Representational] +Pair = [Representational, Representational] +Const2 = [Representational, Phantom, Phantom] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(15), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(15), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(15), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.purs new file mode 100644 index 00000000..d6f1526a --- /dev/null +++ b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Foldable (class Foldable) +import Data.Bifoldable (class Bifoldable) + +data WrapBoth f g a b = WrapBoth (f a) (g b) +derive instance (Foldable f, Foldable g) => Bifoldable (WrapBoth f g) + +data WrapBothNoConstraint f g a b = WrapBothNoConstraint (f a) (g b) +derive instance Bifoldable (WrapBothNoConstraint f g) diff --git a/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap new file mode 100644 index 00000000..23907a00 --- /dev/null +++ b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap @@ -0,0 +1,62 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +WrapBoth :: + forall (t5 :: Type) (t4 :: Type) (f :: (t5 :: Type) -> Type) (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) (b :: (t4 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + (g :: (t4 :: Type) -> Type) (b :: (t4 :: Type)) -> + WrapBoth @(t5 :: Type) @(t4 :: Type) + (f :: (t5 :: Type) -> Type) + (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) + (b :: (t4 :: Type)) +WrapBothNoConstraint :: + forall (t11 :: Type) (t10 :: Type) (f :: (t11 :: Type) -> Type) (g :: (t10 :: Type) -> Type) + (a :: (t11 :: Type)) (b :: (t10 :: Type)). + (f :: (t11 :: Type) -> Type) (a :: (t11 :: Type)) -> + (g :: (t10 :: Type) -> Type) (b :: (t10 :: Type)) -> + WrapBothNoConstraint @(t11 :: Type) @(t10 :: Type) + (f :: (t11 :: Type) -> Type) + (g :: (t10 :: Type) -> Type) + (a :: (t11 :: Type)) + (b :: (t10 :: Type)) + +Types +WrapBoth :: + forall (t5 :: Type) (t4 :: Type). + ((t5 :: Type) -> Type) -> ((t4 :: Type) -> Type) -> (t5 :: Type) -> (t4 :: Type) -> Type +WrapBothNoConstraint :: + forall (t11 :: Type) (t10 :: Type). + ((t11 :: Type) -> Type) -> ((t10 :: Type) -> Type) -> (t11 :: Type) -> (t10 :: Type) -> Type + +Roles +WrapBoth = [Representational, Representational, Nominal, Nominal] +WrapBothNoConstraint = [Representational, Representational, Nominal, Nominal] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(15), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(15), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index d6f07e92..569b139a 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -231,3 +231,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_100_derive_foldable_simple_main() { run_test("100_derive_foldable_simple", "Main"); } #[rustfmt::skip] #[test] fn test_101_derive_foldable_higher_kinded_main() { run_test("101_derive_foldable_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_102_derive_bifoldable_simple_main() { run_test("102_derive_bifoldable_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_103_derive_bifoldable_higher_kinded_main() { run_test("103_derive_bifoldable_higher_kinded", "Main"); } From fa230029dead8aba4e46e2b7afe1a811a0b5635b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:38:03 +0800 Subject: [PATCH 324/386] Implement deriving for Bifoldable --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../checking2/src/source/derive/foldable.rs | 38 +++++++++++++++++++ .../checking2/src/source/derive/head.rs | 7 ++++ .../102_derive_bifoldable_simple/Main.snap | 35 ----------------- .../Main.snap | 30 ++++++++++++--- 5 files changed, 73 insertions(+), 43 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 317abc69..1e748151 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -32,6 +32,7 @@ enum DeriveDispatch { Contravariant, Profunctor, Foldable, + Bifoldable, Ord, Ord1, SupportedButNotImplemented, @@ -91,12 +92,13 @@ where DeriveDispatch::Profunctor } else if class == context.known_types.foldable { DeriveDispatch::Foldable + } else if class == context.known_types.bifoldable { + DeriveDispatch::Bifoldable } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.bifoldable - || class == context.known_types.traversable + } else if class == context.known_types.traversable || class == context.known_types.bitraversable || class == context.known_types.newtype || class == context.known_types.generic diff --git a/compiler-core/checking2/src/source/derive/foldable.rs b/compiler-core/checking2/src/source/derive/foldable.rs index 7c8c6dc3..cac60978 100644 --- a/compiler-core/checking2/src/source/derive/foldable.rs +++ b/compiler-core/checking2/src/source/derive/foldable.rs @@ -48,3 +48,41 @@ where config, })) } + +pub(super) fn check_derive_bifoldable( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let wrapper = context.known_types.foldable; + let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index eba21349..74308cbe 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -214,6 +214,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Bifoldable => foldable::check_derive_bifoldable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap index 823c09ca..0f50c456 100644 --- a/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap +++ b/tests-integration/fixtures/checking2/102_derive_bifoldable_simple/Main.snap @@ -21,38 +21,3 @@ Roles Either = [Representational, Representational] Pair = [Representational, Representational] Const2 = [Representational, Phantom, Phantom] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(15), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(15), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(15), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap index 23907a00..d8c38686 100644 --- a/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/103_derive_bifoldable_higher_kinded/Main.snap @@ -39,9 +39,8 @@ WrapBothNoConstraint = [Representational, Representational, Nominal, Nominal] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(15), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(9), }, crumbs: [ TermDeclaration( @@ -50,9 +49,28 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(15), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), }, crumbs: [ TermDeclaration( From 80d898839090518c27053fac4adeed87c3e4e471 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:39:18 +0800 Subject: [PATCH 325/386] Add smoke tests for Traversable --- .../104_derive_traversable_simple/Main.purs | 20 +++++ .../104_derive_traversable_simple/Main.snap | 57 ++++++++++++++ .../Main.purs | 10 +++ .../Main.snap | 75 +++++++++++++++++++ .../Main.purs | 6 ++ .../Main.snap | 35 +++++++++ .../tests/checking2/generated.rs | 6 ++ 7 files changed, 209 insertions(+) create mode 100644 tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.purs create mode 100644 tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap diff --git a/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.purs b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.purs new file mode 100644 index 00000000..a374bd51 --- /dev/null +++ b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.purs @@ -0,0 +1,20 @@ +module Main where + +import Data.Functor (class Functor) +import Data.Foldable (class Foldable) +import Data.Traversable (class Traversable) + +data Identity a = Identity a +derive instance Functor Identity +derive instance Foldable Identity +derive instance Traversable Identity + +data Maybe a = Nothing | Just a +derive instance Functor Maybe +derive instance Foldable Maybe +derive instance Traversable Maybe + +data Const e a = Const e +derive instance Functor (Const e) +derive instance Foldable (Const e) +derive instance Traversable (Const e) diff --git a/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap new file mode 100644 index 00000000..27d43524 --- /dev/null +++ b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Const :: + forall (t4 :: Type) (e :: Type) (a :: (t4 :: Type)). + (e :: Type) -> Const @(t4 :: Type) (e :: Type) (a :: (t4 :: Type)) + +Types +Identity :: Type -> Type +Maybe :: Type -> Type +Const :: forall (t4 :: Type). Type -> (t4 :: Type) -> Type + +Roles +Identity = [Representational] +Maybe = [Representational] +Const = [Representational, Phantom] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(32), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(32), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(32), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(12), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.purs new file mode 100644 index 00000000..d2402adf --- /dev/null +++ b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Functor (class Functor) +import Data.Foldable (class Foldable) +import Data.Traversable (class Traversable) + +data Compose f g a = Compose (f (g a)) +derive instance (Functor f, Functor g) => Functor (Compose f g) +derive instance (Foldable f, Foldable g) => Foldable (Compose f g) +derive instance (Traversable f, Traversable g) => Traversable (Compose f g) diff --git a/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap new file mode 100644 index 00000000..0633ce13 --- /dev/null +++ b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap @@ -0,0 +1,75 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Compose :: + forall (t4 :: Type) (t3 :: Type) (f :: (t4 :: Type) -> Type) (g :: (t3 :: Type) -> (t4 :: Type)) + (a :: (t3 :: Type)). + (f :: (t4 :: Type) -> Type) ((g :: (t3 :: Type) -> (t4 :: Type)) (a :: (t3 :: Type))) -> + Compose @(t4 :: Type) @(t3 :: Type) + (f :: (t4 :: Type) -> Type) + (g :: (t3 :: Type) -> (t4 :: Type)) + (a :: (t3 :: Type)) + +Types +Compose :: + forall (t4 :: Type) (t3 :: Type). + ((t4 :: Type) -> Type) -> ((t3 :: Type) -> (t4 :: Type)) -> (t3 :: Type) -> Type + +Roles +Compose = [Representational, Representational, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(32), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.purs b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.purs new file mode 100644 index 00000000..cc6bb64b --- /dev/null +++ b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.purs @@ -0,0 +1,6 @@ +module Main where + +import Data.Traversable (class Traversable) + +data Compose f g a = Compose (f (g a)) +derive instance (Traversable f, Traversable g) => Traversable (Compose f g) diff --git a/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap new file mode 100644 index 00000000..8a51673b --- /dev/null +++ b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap @@ -0,0 +1,35 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Compose :: + forall (t4 :: Type) (t3 :: Type) (f :: (t4 :: Type) -> Type) (g :: (t3 :: Type) -> (t4 :: Type)) + (a :: (t3 :: Type)). + (f :: (t4 :: Type) -> Type) ((g :: (t3 :: Type) -> (t4 :: Type)) (a :: (t3 :: Type))) -> + Compose @(t4 :: Type) @(t3 :: Type) + (f :: (t4 :: Type) -> Type) + (g :: (t3 :: Type) -> (t4 :: Type)) + (a :: (t3 :: Type)) + +Types +Compose :: + forall (t4 :: Type) (t3 :: Type). + ((t4 :: Type) -> Type) -> ((t3 :: Type) -> (t4 :: Type)) -> (t3 :: Type) -> Type + +Roles +Compose = [Representational, Representational, Nominal] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(32), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 569b139a..cae73a97 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -235,3 +235,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_102_derive_bifoldable_simple_main() { run_test("102_derive_bifoldable_simple", "Main"); } #[rustfmt::skip] #[test] fn test_103_derive_bifoldable_higher_kinded_main() { run_test("103_derive_bifoldable_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_104_derive_traversable_simple_main() { run_test("104_derive_traversable_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_105_derive_traversable_higher_kinded_main() { run_test("105_derive_traversable_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_106_derive_traversable_missing_superclass_main() { run_test("106_derive_traversable_missing_superclass", "Main"); } From 105e0c56af037c1a52e3ce4f46869b54a3cdcb75 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:39:50 +0800 Subject: [PATCH 326/386] Implement deriving for Traversable --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../checking2/src/source/derive/head.rs | 9 +++- .../src/source/derive/traversable.rs | 49 +++++++++++++++++++ .../104_derive_traversable_simple/Main.snap | 35 ------------- .../Main.snap | 15 ++++-- .../Main.snap | 35 +++++++++++-- 6 files changed, 105 insertions(+), 44 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 1e748151..20410437 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -33,6 +33,7 @@ enum DeriveDispatch { Profunctor, Foldable, Bifoldable, + Traversable, Ord, Ord1, SupportedButNotImplemented, @@ -94,12 +95,13 @@ where DeriveDispatch::Foldable } else if class == context.known_types.bifoldable { DeriveDispatch::Bifoldable + } else if class == context.known_types.traversable { + DeriveDispatch::Traversable } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.traversable - || class == context.known_types.bitraversable + } else if class == context.known_types.bitraversable || class == context.known_types.newtype || class == context.known_types.generic { diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 74308cbe..a3646497 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -12,7 +12,7 @@ use crate::state::CheckState; use super::{ DeriveDispatch, DeriveHeadResult, DeriveStrategy, contravariant, derive_dispatch, eq1_ord1, - eq_ord, foldable, functor, + eq_ord, foldable, functor, traversable, }; pub fn check_derive_declarations( @@ -221,6 +221,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Traversable => traversable::check_derive_traversable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/compiler-core/checking2/src/source/derive/traversable.rs b/compiler-core/checking2/src/source/derive/traversable.rs index 8b137891..a1cc6639 100644 --- a/compiler-core/checking2/src/source/derive/traversable.rs +++ b/compiler-core/checking2/src/source/derive/traversable.rs @@ -1 +1,50 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::toolkit; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; +use super::variance::{Variance, VarianceConfig}; + +pub(super) fn check_derive_traversable( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let traversable = Some((class_file, class_id)); + let config = VarianceConfig::Single((Variance::Covariant, traversable)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap index 27d43524..873cf2b9 100644 --- a/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap +++ b/tests-integration/fixtures/checking2/104_derive_traversable_simple/Main.snap @@ -20,38 +20,3 @@ Roles Identity = [Representational] Maybe = [Representational] Const = [Representational, Phantom] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(32), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(32), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(8), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(32), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(12), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap index 0633ce13..432b1645 100644 --- a/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/105_derive_traversable_higher_kinded/Main.snap @@ -63,9 +63,18 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(32), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(14), }, crumbs: [ TermDeclaration( diff --git a/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap index 8a51673b..bbbab64e 100644 --- a/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap +++ b/tests-integration/fixtures/checking2/106_derive_traversable_missing_superclass/Main.snap @@ -23,9 +23,38 @@ Compose = [Representational, Representational, Nominal] Errors CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(32), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), }, crumbs: [ TermDeclaration( From 00554cee7c4bd9339e31dcbb81b3b3e0bc98bba2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:41:01 +0800 Subject: [PATCH 327/386] Add smoke tests for Bitraversable --- .../107_derive_bitraversable_simple/Main.purs | 15 ++++ .../107_derive_bitraversable_simple/Main.snap | 41 ++++++++++ .../Main.purs | 13 ++++ .../Main.snap | 77 +++++++++++++++++++ .../tests/checking2/generated.rs | 4 + 5 files changed, 150 insertions(+) create mode 100644 tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap diff --git a/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.purs b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.purs new file mode 100644 index 00000000..9f0dc4ba --- /dev/null +++ b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.purs @@ -0,0 +1,15 @@ +module Main where + +import Data.Bifunctor (class Bifunctor) +import Data.Bifoldable (class Bifoldable) +import Data.Bitraversable (class Bitraversable) + +data Either a b = Left a | Right b +derive instance Bifunctor Either +derive instance Bifoldable Either +derive instance Bitraversable Either + +data Pair a b = Pair a b +derive instance Bifunctor Pair +derive instance Bifoldable Pair +derive instance Bitraversable Pair diff --git a/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap new file mode 100644 index 00000000..9a80e3a9 --- /dev/null +++ b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap @@ -0,0 +1,41 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +Pair :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Pair (a :: Type) (b :: Type) + +Types +Either :: Type -> Type -> Type +Pair :: Type -> Type -> Type + +Roles +Either = [Representational, Representational] +Pair = [Representational, Representational] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(17), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(17), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.purs new file mode 100644 index 00000000..00777010 --- /dev/null +++ b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.purs @@ -0,0 +1,13 @@ +module Main where + +import Data.Functor (class Functor) +import Data.Bifunctor (class Bifunctor) +import Data.Foldable (class Foldable) +import Data.Bifoldable (class Bifoldable) +import Data.Traversable (class Traversable) +import Data.Bitraversable (class Bitraversable) + +data WrapBoth f g a b = WrapBoth (f a) (g b) +derive instance (Functor f, Functor g) => Bifunctor (WrapBoth f g) +derive instance (Foldable f, Foldable g) => Bifoldable (WrapBoth f g) +derive instance (Traversable f, Traversable g) => Bitraversable (WrapBoth f g) diff --git a/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap new file mode 100644 index 00000000..dba85866 --- /dev/null +++ b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap @@ -0,0 +1,77 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +WrapBoth :: + forall (t5 :: Type) (t4 :: Type) (f :: (t5 :: Type) -> Type) (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) (b :: (t4 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + (g :: (t4 :: Type) -> Type) (b :: (t4 :: Type)) -> + WrapBoth @(t5 :: Type) @(t4 :: Type) + (f :: (t5 :: Type) -> Type) + (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) + (b :: (t4 :: Type)) + +Types +WrapBoth :: + forall (t5 :: Type) (t4 :: Type). + ((t5 :: Type) -> Type) -> ((t4 :: Type) -> Type) -> (t5 :: Type) -> (t4 :: Type) -> Type + +Roles +WrapBoth = [Representational, Representational, Nominal, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(14), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(15), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(17), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index cae73a97..1067739a 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -241,3 +241,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_105_derive_traversable_higher_kinded_main() { run_test("105_derive_traversable_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_106_derive_traversable_missing_superclass_main() { run_test("106_derive_traversable_missing_superclass", "Main"); } + +#[rustfmt::skip] #[test] fn test_107_derive_bitraversable_simple_main() { run_test("107_derive_bitraversable_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_108_derive_bitraversable_higher_kinded_main() { run_test("108_derive_bitraversable_higher_kinded", "Main"); } From a5847af36b31f062af001fd860925eab548f885e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:41:38 +0800 Subject: [PATCH 328/386] Implement deriving for Bitraversable --- compiler-core/checking2/src/source/derive.rs | 6 ++- .../checking2/src/source/derive/head.rs | 7 ++++ .../src/source/derive/traversable.rs | 38 +++++++++++++++++++ .../107_derive_bitraversable_simple/Main.snap | 24 ------------ .../Main.snap | 15 ++++++-- 5 files changed, 61 insertions(+), 29 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 20410437..5a040c06 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -34,6 +34,7 @@ enum DeriveDispatch { Foldable, Bifoldable, Traversable, + Bitraversable, Ord, Ord1, SupportedButNotImplemented, @@ -97,12 +98,13 @@ where DeriveDispatch::Bifoldable } else if class == context.known_types.traversable { DeriveDispatch::Traversable + } else if class == context.known_types.bitraversable { + DeriveDispatch::Bitraversable } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.bitraversable - || class == context.known_types.newtype + } else if class == context.known_types.newtype || class == context.known_types.generic { DeriveDispatch::SupportedButNotImplemented diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index a3646497..478b6f74 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -228,6 +228,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Bitraversable => traversable::check_derive_bitraversable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, diff --git a/compiler-core/checking2/src/source/derive/traversable.rs b/compiler-core/checking2/src/source/derive/traversable.rs index a1cc6639..fa5ddb47 100644 --- a/compiler-core/checking2/src/source/derive/traversable.rs +++ b/compiler-core/checking2/src/source/derive/traversable.rs @@ -48,3 +48,41 @@ where config, })) } + +pub(super) fn check_derive_bitraversable( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[crate::core::TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 1, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let wrapper = context.known_types.traversable; + let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + + Ok(Some(DeriveStrategy::VarianceConstraints { + data_file, + data_id, + derived_type: *derived_type, + config, + })) +} diff --git a/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap index 9a80e3a9..0115f6f9 100644 --- a/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap +++ b/tests-integration/fixtures/checking2/107_derive_bitraversable_simple/Main.snap @@ -15,27 +15,3 @@ Pair :: Type -> Type -> Type Roles Either = [Representational, Representational] Pair = [Representational, Representational] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(17), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(17), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(8), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap index dba85866..a7eb3475 100644 --- a/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/108_derive_bitraversable_higher_kinded/Main.snap @@ -65,9 +65,18 @@ CheckError { ], } CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(17), - class_id: Idx::(0), + kind: NoInstanceFound { + constraint: Id(16), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(17), }, crumbs: [ TermDeclaration( From 7a0ec6fd00097b0cc974c17e23e21084dd1cacbb Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:43:07 +0800 Subject: [PATCH 329/386] Add smoke tests for derive newtype --- .../109_derive_newtype_simple/Main.purs | 7 +++++ .../109_derive_newtype_simple/Main.snap | 26 +++++++++++++++++++ .../Main.purs | 7 +++++ .../Main.snap | 26 +++++++++++++++++++ .../111_derive_newtype_not_newtype/Main.purs | 7 +++++ .../111_derive_newtype_not_newtype/Main.snap | 26 +++++++++++++++++++ .../tests/checking2/generated.rs | 6 +++++ 7 files changed, 105 insertions(+) create mode 100644 tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.purs create mode 100644 tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap create mode 100644 tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.purs create mode 100644 tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap diff --git a/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.purs b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.purs new file mode 100644 index 00000000..87027037 --- /dev/null +++ b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Wrapper = Wrapper Int + +derive newtype instance Show Wrapper diff --git a/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap new file mode 100644 index 00000000..51d756de --- /dev/null +++ b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrapper :: Int -> Wrapper + +Types +Wrapper :: Type + +Roles +Wrapper = [] + +Errors +CheckError { + kind: CannotDeriveClass { + class_file: Idx::(30), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.purs b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.purs new file mode 100644 index 00000000..439202ce --- /dev/null +++ b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Identity a = Identity a + +derive newtype instance Show (Identity Int) diff --git a/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap new file mode 100644 index 00000000..f56a2894 --- /dev/null +++ b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) + +Types +Identity :: Type -> Type + +Roles +Identity = [Representational] + +Errors +CheckError { + kind: CannotDeriveClass { + class_file: Idx::(30), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.purs b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.purs new file mode 100644 index 00000000..8092e314 --- /dev/null +++ b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +data Foo = Foo Int + +derive newtype instance Show Foo diff --git a/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap new file mode 100644 index 00000000..7d9e44ea --- /dev/null +++ b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Foo :: Int -> Foo + +Types +Foo :: Type + +Roles +Foo = [] + +Errors +CheckError { + kind: CannotDeriveClass { + class_file: Idx::(30), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 1067739a..0a08a6e6 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -245,3 +245,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_107_derive_bitraversable_simple_main() { run_test("107_derive_bitraversable_simple", "Main"); } #[rustfmt::skip] #[test] fn test_108_derive_bitraversable_higher_kinded_main() { run_test("108_derive_bitraversable_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_109_derive_newtype_simple_main() { run_test("109_derive_newtype_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_110_derive_newtype_parameterized_main() { run_test("110_derive_newtype_parameterized", "Main"); } + +#[rustfmt::skip] #[test] fn test_111_derive_newtype_not_newtype_main() { run_test("111_derive_newtype_not_newtype", "Main"); } From 276ce4a755a836e09befb4fdc47877c1e7b9bc6a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:43:48 +0800 Subject: [PATCH 330/386] Implement 'derive newtype' --- compiler-core/checking2/src/source/derive.rs | 3 + .../checking2/src/source/derive/head.rs | 190 ++++++++++-------- .../checking2/src/source/derive/member.rs | 4 + .../checking2/src/source/derive/newtype.rs | 56 ++++++ .../109_derive_newtype_simple/Main.snap | 13 -- .../Main.snap | 13 -- .../111_derive_newtype_not_newtype/Main.snap | 5 +- 7 files changed, 167 insertions(+), 117 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 5a040c06..014af041 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -53,6 +53,9 @@ pub(super) enum DeriveStrategy { derived_type: TypeId, class: (FileId, TypeItemId), }, + NewtypeDeriveConstraint { + delegate_constraint: TypeId, + }, VarianceConstraints { data_file: FileId, data_id: TypeItemId, diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 478b6f74..094e1582 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -12,7 +12,7 @@ use crate::state::CheckState; use super::{ DeriveDispatch, DeriveHeadResult, DeriveStrategy, contravariant, derive_dispatch, eq1_ord1, - eq_ord, foldable, functor, traversable, + eq_ord, foldable, functor, newtype, traversable, }; pub fn check_derive_declarations( @@ -29,11 +29,11 @@ where let items = items.iter().filter_map(|&item_id| { let item = context.lowered.info.get_term_item(item_id)?; - let TermItemIr::Derive { constraints, resolution, arguments, .. } = item else { + let TermItemIr::Derive { newtype, constraints, resolution, arguments } = item else { return None; }; let resolution = *resolution; - Some(CheckDeriveDeclaration { item_id, constraints, resolution, arguments }) + Some(CheckDeriveDeclaration { item_id, newtype: *newtype, constraints, resolution, arguments }) }); for item in items { @@ -48,6 +48,7 @@ where struct CheckDeriveDeclaration<'a> { item_id: TermItemId, + newtype: bool, constraints: &'a [lowering::TypeId], resolution: Option<(FileId, TypeItemId)>, arguments: &'a [lowering::TypeId], @@ -56,6 +57,7 @@ struct CheckDeriveDeclaration<'a> { struct CheckDeriveDeclarationCore<'a> { derive_id: indexing::DeriveId, item_id: TermItemId, + newtype: bool, class_file: FileId, class_id: TypeItemId, constraints: &'a [lowering::TypeId], @@ -70,7 +72,7 @@ fn check_derive_declaration( where Q: ExternalQueries, { - let CheckDeriveDeclaration { item_id, constraints, resolution, arguments } = item; + let CheckDeriveDeclaration { item_id, newtype, constraints, resolution, arguments } = item; let Some((class_file, class_id)) = resolution else { return Ok(None); @@ -84,6 +86,7 @@ where check_derive_declaration_core(state, context, CheckDeriveDeclarationCore { derive_id, item_id, + newtype, class_file, class_id, constraints, @@ -103,6 +106,7 @@ where let CheckDeriveDeclarationCore { derive_id, item_id, + newtype, class_file, class_id, constraints, @@ -164,95 +168,105 @@ where state.checked.derived.insert(derive_id, CheckedInstance { resolution, canonical }); - let strategy = match derive_dispatch(context, class_file, class_id) { - DeriveDispatch::Eq => eq_ord::check_derive_eq( + let strategy = if newtype { + newtype::check_derive_newtype( state, context, class_file, class_id, &checked_arguments, - )?, - DeriveDispatch::Eq1 => eq1_ord1::check_derive_eq1( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Functor => functor::check_derive_functor( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Bifunctor => functor::check_derive_bifunctor( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Contravariant => contravariant::check_derive_contravariant( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Profunctor => contravariant::check_derive_profunctor( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Foldable => foldable::check_derive_foldable( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Bifoldable => foldable::check_derive_bifoldable( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Traversable => traversable::check_derive_traversable( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Bitraversable => traversable::check_derive_bitraversable( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Ord => eq_ord::check_derive_ord( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::Ord1 => eq1_ord1::check_derive_ord1( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, - DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), - DeriveDispatch::Unsupported => { - state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); - None + )? + } else { + match derive_dispatch(context, class_file, class_id) { + DeriveDispatch::Eq => eq_ord::check_derive_eq( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Eq1 => eq1_ord1::check_derive_eq1( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Functor => functor::check_derive_functor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Bifunctor => functor::check_derive_bifunctor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Contravariant => contravariant::check_derive_contravariant( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Profunctor => contravariant::check_derive_profunctor( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Foldable => foldable::check_derive_foldable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Bifoldable => foldable::check_derive_bifoldable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Traversable => traversable::check_derive_traversable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Bitraversable => traversable::check_derive_bitraversable( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Ord => eq_ord::check_derive_ord( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::Ord1 => eq1_ord1::check_derive_ord1( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, + DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), + DeriveDispatch::Unsupported => { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + None + } } }; diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index e0e661a4..a1bdfdcd 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -66,6 +66,10 @@ where generate_delegate_constraint(state, context, derived_type, class); tools::solve_and_report_constraints(state, context)?; } + DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint } => { + state.push_wanted(delegate_constraint); + tools::solve_and_report_constraints(state, context)?; + } DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config } => { tools::emit_superclass_constraints( state, diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs index 8b137891..09089541 100644 --- a/compiler-core/checking2/src/source/derive/newtype.rs +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -1 +1,57 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TypeItemId; +use crate::ExternalQueries; +use crate::context::CheckContext; +use crate::core::{Type, TypeId, toolkit}; +use crate::error::ErrorKind; +use crate::state::CheckState; + +use super::DeriveStrategy; + +pub(super) fn check_derive_newtype( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: TypeItemId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [preceding_arguments @ .., newtype_type] = arguments else { + return Ok(None); + }; + + let Some((newtype_file, newtype_id)) = + toolkit::extract_type_constructor(state, context, *newtype_type)? + else { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + if newtype_file != context.id || !toolkit::is_newtype(context, newtype_file, newtype_id)? { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + return Ok(None); + } + + let Some(inner_type) = + toolkit::get_newtype_inner(state, context, newtype_file, newtype_id, *newtype_type)? + else { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + return Ok(None); + }; + + let class_type = context.queries.intern_type(Type::Constructor(class_file, class_id)); + let mut delegate_constraint = preceding_arguments + .iter() + .copied() + .fold(class_type, |function, argument| context.intern_application(function, argument)); + delegate_constraint = context.intern_application(delegate_constraint, inner_type); + + Ok(Some(DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint })) +} diff --git a/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap index 51d756de..309c4718 100644 --- a/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap +++ b/tests-integration/fixtures/checking2/109_derive_newtype_simple/Main.snap @@ -11,16 +11,3 @@ Wrapper :: Type Roles Wrapper = [] - -Errors -CheckError { - kind: CannotDeriveClass { - class_file: Idx::(30), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap index f56a2894..e66717ce 100644 --- a/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap +++ b/tests-integration/fixtures/checking2/110_derive_newtype_parameterized/Main.snap @@ -11,16 +11,3 @@ Identity :: Type -> Type Roles Identity = [Representational] - -Errors -CheckError { - kind: CannotDeriveClass { - class_file: Idx::(30), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap index 7d9e44ea..fd5c52eb 100644 --- a/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap +++ b/tests-integration/fixtures/checking2/111_derive_newtype_not_newtype/Main.snap @@ -14,9 +14,8 @@ Foo = [] Errors CheckError { - kind: CannotDeriveClass { - class_file: Idx::(30), - class_id: Idx::(0), + kind: ExpectedNewtype { + type_message: Id(7), }, crumbs: [ TermDeclaration( From 9cb92f1e611f9a7167a23af1d6d0f3b515382051 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:46:13 +0800 Subject: [PATCH 331/386] Implement deriving for Newtype --- compiler-core/checking2/src/source/derive.rs | 7 ++- .../checking2/src/source/derive/head.rs | 19 +++++--- .../checking2/src/source/derive/member.rs | 10 +++++ .../checking2/src/source/derive/newtype.rs | 43 ++++++++++++++++++- .../112_derive_newtype_class_simple/Main.purs | 7 +++ .../112_derive_newtype_class_simple/Main.snap | 13 ++++++ .../Main.purs | 7 +++ .../Main.snap | 13 ++++++ .../Main.purs | 7 +++ .../Main.snap | 25 +++++++++++ .../tests/checking2/generated.rs | 6 +++ 11 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.snap create mode 100644 tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.purs create mode 100644 tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.snap create mode 100644 tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.purs create mode 100644 tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.snap diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 014af041..314dedb4 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -35,6 +35,7 @@ enum DeriveDispatch { Bifoldable, Traversable, Bitraversable, + Newtype, Ord, Ord1, SupportedButNotImplemented, @@ -56,6 +57,7 @@ pub(super) enum DeriveStrategy { NewtypeDeriveConstraint { delegate_constraint: TypeId, }, + HeadOnly, VarianceConstraints { data_file: FileId, data_id: TypeItemId, @@ -103,12 +105,13 @@ where DeriveDispatch::Traversable } else if class == context.known_types.bitraversable { DeriveDispatch::Bitraversable + } else if class == context.known_types.newtype { + DeriveDispatch::Newtype } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.newtype - || class == context.known_types.generic + } else if class == context.known_types.generic { DeriveDispatch::SupportedButNotImplemented } else { diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 094e1582..812ec593 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -162,12 +162,6 @@ where constraint::instances::validate_rows(state, context, class_file, class_id, &checked_arguments)?; - let resolution = (class_file, class_id); - let canonical = zonk::zonk(state, context, canonical)?; - let canonical = generalise::generalise_implicit(state, context, canonical)?; - - state.checked.derived.insert(derive_id, CheckedInstance { resolution, canonical }); - let strategy = if newtype { newtype::check_derive_newtype( state, @@ -248,6 +242,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Newtype => newtype::check_derive_newtype_class( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, @@ -270,6 +271,12 @@ where } }; + let resolution = (class_file, class_id); + let canonical = zonk::zonk(state, context, canonical)?; + let canonical = generalise::generalise_implicit(state, context, canonical)?; + + state.checked.derived.insert(derive_id, CheckedInstance { resolution, canonical }); + Ok(strategy.map(|strategy| DeriveHeadResult { item_id, constraints: checked_constraints, diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index a1bdfdcd..da318781 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -70,6 +70,16 @@ where state.push_wanted(delegate_constraint); tools::solve_and_report_constraints(state, context)?; } + DeriveStrategy::HeadOnly => { + tools::emit_superclass_constraints( + state, + context, + result.class_file, + result.class_id, + &result.arguments, + )?; + tools::solve_and_report_constraints(state, context)?; + } DeriveStrategy::VarianceConstraints { data_file, data_id, derived_type, config } => { tools::emit_superclass_constraints( state, diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs index 09089541..9817bc8e 100644 --- a/compiler-core/checking2/src/source/derive/newtype.rs +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -4,7 +4,7 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{Type, TypeId, toolkit}; +use crate::core::{Type, TypeId, toolkit, unification}; use crate::error::ErrorKind; use crate::state::CheckState; @@ -55,3 +55,44 @@ where Ok(Some(DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint })) } + +pub(super) fn check_derive_newtype_class( + state: &mut CheckState, + context: &CheckContext, + _class_file: FileId, + _class_id: TypeItemId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [newtype_type, inner_type] = arguments else { + return Ok(None); + }; + + let Some((newtype_file, newtype_id)) = + toolkit::extract_type_constructor(state, context, *newtype_type)? + else { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + if newtype_file != context.id || !toolkit::is_newtype(context, newtype_file, newtype_id)? { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + return Ok(None); + } + + let Some(extracted_inner) = + toolkit::get_newtype_inner(state, context, newtype_file, newtype_id, *newtype_type)? + else { + let type_message = state.pretty_id(context, *newtype_type)?; + state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + return Ok(None); + }; + + unification::unify(state, context, *inner_type, extracted_inner)?; + + Ok(Some(DeriveStrategy::HeadOnly)) +} diff --git a/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.purs b/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.purs new file mode 100644 index 00000000..b48f3b16 --- /dev/null +++ b/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Newtype (class Newtype) + +newtype UserId = UserId Int + +derive instance Newtype UserId _ diff --git a/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.snap b/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.snap new file mode 100644 index 00000000..cd2243b9 --- /dev/null +++ b/tests-integration/fixtures/checking2/112_derive_newtype_class_simple/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +UserId :: Int -> UserId + +Types +UserId :: Type + +Roles +UserId = [] diff --git a/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.purs b/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.purs new file mode 100644 index 00000000..c6284bd3 --- /dev/null +++ b/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Newtype (class Newtype) + +newtype Wrapper a = Wrapper a + +derive instance Newtype (Wrapper a) _ diff --git a/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.snap b/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.snap new file mode 100644 index 00000000..e9dfac50 --- /dev/null +++ b/tests-integration/fixtures/checking2/113_derive_newtype_class_parameterized/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrapper :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) + +Types +Wrapper :: Type -> Type + +Roles +Wrapper = [Representational] diff --git a/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.purs b/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.purs new file mode 100644 index 00000000..18a518ba --- /dev/null +++ b/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Newtype (class Newtype) + +data NotANewtype = NotANewtype Int + +derive instance Newtype NotANewtype _ diff --git a/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.snap b/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.snap new file mode 100644 index 00000000..204ad3d5 --- /dev/null +++ b/tests-integration/fixtures/checking2/114_derive_newtype_class_not_newtype/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +NotANewtype :: Int -> NotANewtype + +Types +NotANewtype :: Type + +Roles +NotANewtype = [] + +Errors +CheckError { + kind: ExpectedNewtype { + type_message: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 0a08a6e6..34c576d6 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -251,3 +251,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_110_derive_newtype_parameterized_main() { run_test("110_derive_newtype_parameterized", "Main"); } #[rustfmt::skip] #[test] fn test_111_derive_newtype_not_newtype_main() { run_test("111_derive_newtype_not_newtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_112_derive_newtype_class_simple_main() { run_test("112_derive_newtype_class_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_113_derive_newtype_class_parameterized_main() { run_test("113_derive_newtype_class_parameterized", "Main"); } + +#[rustfmt::skip] #[test] fn test_114_derive_newtype_class_not_newtype_main() { run_test("114_derive_newtype_class_not_newtype", "Main"); } From 83d3be9503a56fd32deda1becedd43116afce8a2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:48:23 +0800 Subject: [PATCH 332/386] Add smoke tests for Generic --- .../115_derive_generic_simple/Main.purs | 44 +++++++ .../115_derive_generic_simple/Main.snap | 122 ++++++++++++++++++ .../tests/checking2/generated.rs | 2 + 3 files changed, 168 insertions(+) create mode 100644 tests-integration/fixtures/checking2/115_derive_generic_simple/Main.purs create mode 100644 tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap diff --git a/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.purs b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.purs new file mode 100644 index 00000000..46f1851b --- /dev/null +++ b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.purs @@ -0,0 +1,44 @@ +module Main where + +import Data.Generic.Rep (class Generic) + +data Void + +data MyUnit = MyUnit + +data Identity a = Identity a + +data Either a b = Left a | Right b + +data Tuple a b = Tuple a b + +newtype Wrapper a = Wrapper a + +derive instance Generic Void _ +derive instance Generic MyUnit _ +derive instance Generic (Identity a) _ +derive instance Generic (Either a b) _ +derive instance Generic (Tuple a b) _ +derive instance Generic (Wrapper a) _ + +data Proxy a = Proxy + +getVoid :: forall rep. Generic Void rep => Proxy rep +getVoid = Proxy + +getMyUnit :: forall rep. Generic MyUnit rep => Proxy rep +getMyUnit = Proxy + +getIdentity :: forall a rep. Generic (Identity a) rep => Proxy rep +getIdentity = Proxy + +getEither :: forall a b rep. Generic (Either a b) rep => Proxy rep +getEither = Proxy + +getTuple :: forall a b rep. Generic (Tuple a b) rep => Proxy rep +getTuple = Proxy + +getWrapper :: forall a rep. Generic (Wrapper a) rep => Proxy rep +getWrapper = Proxy + +forceSolve = { getVoid, getMyUnit, getIdentity, getEither, getTuple, getWrapper } diff --git a/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap new file mode 100644 index 00000000..4c711613 --- /dev/null +++ b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap @@ -0,0 +1,122 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +MyUnit :: MyUnit +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +Wrapper :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) +Proxy :: forall (t7 :: Type) (a :: (t7 :: Type)). Proxy @(t7 :: Type) (a :: (t7 :: Type)) +getVoid :: forall (rep :: Type). Generic Void (rep :: Type) => Proxy @Type (rep :: Type) +getMyUnit :: forall (rep :: Type). Generic MyUnit (rep :: Type) => Proxy @Type (rep :: Type) +getIdentity :: + forall (a :: Type) (rep :: Type). + Generic (Identity (a :: Type)) (rep :: Type) => Proxy @Type (rep :: Type) +getEither :: + forall (a :: Type) (b :: Type) (rep :: Type). + Generic (Either (a :: Type) (b :: Type)) (rep :: Type) => Proxy @Type (rep :: Type) +getTuple :: + forall (a :: Type) (b :: Type) (rep :: Type). + Generic (Tuple (a :: Type) (b :: Type)) (rep :: Type) => Proxy @Type (rep :: Type) +getWrapper :: + forall (a :: Type) (rep :: Type). + Generic (Wrapper (a :: Type)) (rep :: Type) => Proxy @Type (rep :: Type) +forceSolve :: + forall (t37 :: Type) (t36 :: Type) (t35 :: Type) (t34 :: Type) (t33 :: Type) (t32 :: Type). + { getEither :: Proxy @Type (t37 :: Type) + , getIdentity :: Proxy @Type (t36 :: Type) + , getMyUnit :: Proxy @Type (t35 :: Type) + , getTuple :: Proxy @Type (t34 :: Type) + , getVoid :: Proxy @Type (t33 :: Type) + , getWrapper :: Proxy @Type (t32 :: Type) + } + +Types +Void :: Type +MyUnit :: Type +Identity :: Type -> Type +Either :: Type -> Type -> Type +Tuple :: Type -> Type -> Type +Wrapper :: Type -> Type +Proxy :: forall (t7 :: Type). (t7 :: Type) -> Type + +Roles +Void = [] +MyUnit = [] +Identity = [Representational] +Either = [Representational, Representational] +Tuple = [Representational, Representational] +Wrapper = [Representational] +Proxy = [Phantom] + +Errors +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(7), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(8), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(9), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(10), + ), + ], +} +CheckError { + kind: DeriveNotSupportedYet { + class_file: Idx::(23), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(11), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 34c576d6..5e079b86 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -257,3 +257,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_113_derive_newtype_class_parameterized_main() { run_test("113_derive_newtype_class_parameterized", "Main"); } #[rustfmt::skip] #[test] fn test_114_derive_newtype_class_not_newtype_main() { run_test("114_derive_newtype_class_not_newtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_115_derive_generic_simple_main() { run_test("115_derive_generic_simple", "Main"); } From a22d0262cbb1ef133782a2f5c5b226e3c8834107 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 3 Mar 2026 13:49:09 +0800 Subject: [PATCH 333/386] Implement deriving for Generic --- compiler-core/checking2/src/source/derive.rs | 8 +- .../checking2/src/source/derive/generic.rs | 188 ++++++++++++++++++ .../checking2/src/source/derive/head.rs | 12 +- .../checking2/src/source/derive/member.rs | 8 +- .../115_derive_generic_simple/Main.snap | 88 ++------ 5 files changed, 214 insertions(+), 90 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 314dedb4..7707141d 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -36,9 +36,9 @@ enum DeriveDispatch { Traversable, Bitraversable, Newtype, + Generic, Ord, Ord1, - SupportedButNotImplemented, Unsupported, } @@ -64,7 +64,6 @@ pub(super) enum DeriveStrategy { derived_type: TypeId, config: VarianceConfig, }, - Unsupported, } pub struct DeriveHeadResult { @@ -107,13 +106,12 @@ where DeriveDispatch::Bitraversable } else if class == context.known_types.newtype { DeriveDispatch::Newtype + } else if class == context.known_types.generic { + DeriveDispatch::Generic } else if class == context.known_types.ord { DeriveDispatch::Ord } else if class == context.known_types.ord1 { DeriveDispatch::Ord1 - } else if class == context.known_types.generic - { - DeriveDispatch::SupportedButNotImplemented } else { DeriveDispatch::Unsupported } diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs index 8b137891..fb74b90b 100644 --- a/compiler-core/checking2/src/source/derive/generic.rs +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -1 +1,189 @@ +use building_types::QueryResult; +use files::FileId; +use indexing::TermItemId; +use lowering::StringKind; +use smol_str::SmolStr; +use crate::ExternalQueries; +use crate::context::{CheckContext, KnownGeneric}; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, toolkit, unification}; +use crate::error::ErrorKind; +use crate::state::CheckState; +use crate::safe_loop; + +use super::{DeriveStrategy, tools}; + +pub(super) fn check_derive_generic( + state: &mut CheckState, + context: &CheckContext, + class_file: FileId, + class_id: indexing::TypeItemId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let [derived_type, wildcard_type] = arguments else { + state.insert_error(ErrorKind::DeriveInvalidArity { + class_file, + class_id, + expected: 2, + actual: arguments.len(), + }); + return Ok(None); + }; + + let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + else { + let type_message = state.pretty_id(context, *derived_type)?; + state.insert_error(ErrorKind::CannotDeriveForType { type_message }); + return Ok(None); + }; + + let Some(ref known_generic) = context.known_generic else { + state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); + return Ok(None); + }; + + let constructors = tools::lookup_data_constructors(context, data_file, data_id)?; + let generic_rep = + build_generic_rep(state, context, known_generic, data_file, *derived_type, &constructors)?; + + let _ = unification::unify(state, context, *wildcard_type, generic_rep)?; + Ok(Some(DeriveStrategy::HeadOnly)) +} + +fn build_generic_rep( + state: &mut CheckState, + context: &CheckContext, + known_generic: &KnownGeneric, + data_file: FileId, + derived_type: TypeId, + constructors: &[TermItemId], +) -> QueryResult +where + Q: ExternalQueries, +{ + let [rest @ .., last] = constructors else { + return Ok(known_generic.no_constructors); + }; + + let arguments = extract_all_applications(state, context, derived_type)?; + let mut rep = + build_generic_constructor(state, context, known_generic, data_file, &arguments, *last)?; + + for &constructor_id in rest.iter().rev() { + let constructor = + build_generic_constructor(state, context, known_generic, data_file, &arguments, constructor_id)?; + let applied = context.intern_application(known_generic.sum, constructor); + rep = context.intern_application(applied, rep); + } + + Ok(rep) +} + +fn build_generic_constructor( + state: &mut CheckState, + context: &CheckContext, + known_generic: &KnownGeneric, + data_file: FileId, + arguments: &[TypeId], + constructor_id: TermItemId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let constructor_name = if data_file == context.id { + context.indexed.items[constructor_id] + .name + .clone() + .unwrap_or_else(|| SmolStr::new_static("")) + } else { + context + .queries + .indexed(data_file)? + .items[constructor_id] + .name + .clone() + .unwrap_or_else(|| SmolStr::new_static("")) + }; + + let constructor_type = toolkit::lookup_file_term(state, context, data_file, constructor_id)?; + let field_types = instantiate_constructor_fields(state, context, constructor_type, arguments)?; + let fields_rep = build_fields_rep(context, known_generic, &field_types); + + let string_id = context.queries.intern_smol_str(constructor_name); + let name = context.queries.intern_type(Type::String(StringKind::String, string_id)); + let constructor = context.intern_application(known_generic.constructor, name); + Ok(context.intern_application(constructor, fields_rep)) +} + +fn build_fields_rep( + context: &CheckContext, + known_generic: &KnownGeneric, + field_types: &[TypeId], +) -> TypeId { + let [rest @ .., last] = field_types else { + return known_generic.no_arguments; + }; + + let mut rep = context.intern_application(known_generic.argument, *last); + for &field in rest.iter().rev() { + let argument = context.intern_application(known_generic.argument, field); + let product = context.intern_application(known_generic.product, argument); + rep = context.intern_application(product, rep); + } + rep +} + +fn extract_all_applications( + state: &mut CheckState, + context: &CheckContext, + mut applied_type: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + safe_loop! { + applied_type = normalise::normalise(state, context, applied_type)?; + match context.lookup_type(applied_type) { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + arguments.push(argument); + applied_type = function; + } + _ => break, + } + } + arguments.reverse(); + Ok(arguments) +} + +fn instantiate_constructor_fields( + state: &mut CheckState, + context: &CheckContext, + constructor_type: TypeId, + arguments: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut current = constructor_type; + let mut arguments = arguments.iter().copied(); + + safe_loop! { + current = normalise::normalise(state, context, current)?; + let Type::Forall(binder_id, inner) = context.lookup_type(current) else { + break; + }; + + let binder = context.lookup_forall_binder(binder_id); + let argument_type = + arguments.next().unwrap_or_else(|| context.intern_rigid(binder.name, state.depth, binder.kind)); + current = SubstituteName::one(state, context, binder.name, argument_type, inner)?; + } + + let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, current)?; + Ok(arguments) +} diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 812ec593..f2827f65 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -11,8 +11,8 @@ use crate::source::types; use crate::state::CheckState; use super::{ - DeriveDispatch, DeriveHeadResult, DeriveStrategy, contravariant, derive_dispatch, eq1_ord1, - eq_ord, foldable, functor, newtype, traversable, + DeriveDispatch, DeriveHeadResult, contravariant, derive_dispatch, eq1_ord1, eq_ord, foldable, + functor, generic, newtype, traversable, }; pub fn check_derive_declarations( @@ -249,6 +249,13 @@ where class_id, &checked_arguments, )?, + DeriveDispatch::Generic => generic::check_derive_generic( + state, + context, + class_file, + class_id, + &checked_arguments, + )?, DeriveDispatch::Ord => eq_ord::check_derive_ord( state, context, @@ -263,7 +270,6 @@ where class_id, &checked_arguments, )?, - DeriveDispatch::SupportedButNotImplemented => Some(DeriveStrategy::Unsupported), DeriveDispatch::Unsupported => { state.insert_error(ErrorKind::CannotDeriveClass { class_file, class_id }); None diff --git a/compiler-core/checking2/src/source/derive/member.rs b/compiler-core/checking2/src/source/derive/member.rs index da318781..c9406aff 100644 --- a/compiler-core/checking2/src/source/derive/member.rs +++ b/compiler-core/checking2/src/source/derive/member.rs @@ -3,7 +3,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::Type; -use crate::error::{ErrorCrumb, ErrorKind}; +use crate::error::ErrorCrumb; use crate::state::CheckState; use super::{DeriveHeadResult, DeriveStrategy, field, tools, variance}; @@ -98,12 +98,6 @@ where )?; tools::solve_and_report_constraints(state, context)?; } - DeriveStrategy::Unsupported => { - state.insert_error(ErrorKind::DeriveNotSupportedYet { - class_file: result.class_file, - class_id: result.class_id, - }); - } } Ok(()) } diff --git a/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap index 4c711613..8d090366 100644 --- a/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap +++ b/tests-integration/fixtures/checking2/115_derive_generic_simple/Main.snap @@ -26,13 +26,19 @@ getWrapper :: forall (a :: Type) (rep :: Type). Generic (Wrapper (a :: Type)) (rep :: Type) => Proxy @Type (rep :: Type) forceSolve :: - forall (t37 :: Type) (t36 :: Type) (t35 :: Type) (t34 :: Type) (t33 :: Type) (t32 :: Type). - { getEither :: Proxy @Type (t37 :: Type) - , getIdentity :: Proxy @Type (t36 :: Type) - , getMyUnit :: Proxy @Type (t35 :: Type) - , getTuple :: Proxy @Type (t34 :: Type) - , getVoid :: Proxy @Type (t33 :: Type) - , getWrapper :: Proxy @Type (t32 :: Type) + forall (t31 :: Type) (t30 :: Type) (t29 :: Type) (t28 :: Type) (t27 :: Type) (t26 :: Type). + { getEither :: + Proxy @Type + (Sum + (Constructor "Left" (Argument (t31 :: Type))) + (Constructor "Right" (Argument (t30 :: Type)))) + , getIdentity :: Proxy @Type (Constructor "Identity" (Argument (t29 :: Type))) + , getMyUnit :: Proxy @Type (Constructor "MyUnit" NoArguments) + , getTuple :: + Proxy @Type + (Constructor "Tuple" (Product (Argument (t28 :: Type)) (Argument (t27 :: Type)))) + , getVoid :: Proxy @Type NoConstructors + , getWrapper :: Proxy @Type (Constructor "Wrapper" (Argument (t26 :: Type))) } Types @@ -52,71 +58,3 @@ Either = [Representational, Representational] Tuple = [Representational, Representational] Wrapper = [Representational] Proxy = [Phantom] - -Errors -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(6), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(7), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(8), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(9), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(10), - ), - ], -} -CheckError { - kind: DeriveNotSupportedYet { - class_file: Idx::(23), - class_id: Idx::(0), - }, - crumbs: [ - TermDeclaration( - Idx::(11), - ), - ], -} From e02ac613effbcfe22f311ba452e6f4060ee1ee50 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 02:49:49 +0800 Subject: [PATCH 334/386] Add tests for derivable type classes --- .../Main.purs | 10 ++ .../Main.snap | 18 ++++ .../Main.purs | 23 +++++ .../Main.snap | 20 ++++ .../118_derive_newtype_with_given/Main.purs | 7 ++ .../118_derive_newtype_with_given/Main.snap | 13 +++ .../Main.purs | 7 ++ .../Main.snap | 25 +++++ .../Main.purs | 7 ++ .../Main.snap | 25 +++++ .../121_derive_newtype_multi_param/Main.purs | 7 ++ .../121_derive_newtype_multi_param/Main.snap | 27 ++++++ .../Main.purs | 11 +++ .../Main.snap | 34 +++++++ .../123_derive_newtype_function/Main.purs | 9 ++ .../123_derive_newtype_function/Main.snap | 36 ++++++++ .../Main.purs | 12 +++ .../Main.snap | 18 ++++ .../125_derive_eq_1_higher_kinded/Main.purs | 11 +++ .../125_derive_eq_1_higher_kinded/Main.snap | 44 +++++++++ .../checking2/126_derive_eq_partial/Main.purs | 9 ++ .../checking2/126_derive_eq_partial/Main.snap | 26 ++++++ .../Main.purs | 24 +++++ .../Main.snap | 91 +++++++++++++++++++ .../Main.purs | 9 ++ .../Main.snap | 44 +++++++++ .../Main.purs | 10 ++ .../Main.snap | 80 ++++++++++++++++ .../tests/checking2/generated.rs | 28 ++++++ 29 files changed, 685 insertions(+) create mode 100644 tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.purs create mode 100644 tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.snap create mode 100644 tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.purs create mode 100644 tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.snap create mode 100644 tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.purs create mode 100644 tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.snap create mode 100644 tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.purs create mode 100644 tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.snap create mode 100644 tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.purs create mode 100644 tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.snap create mode 100644 tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.purs create mode 100644 tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap create mode 100644 tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/123_derive_newtype_function/Main.purs create mode 100644 tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap create mode 100644 tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.purs create mode 100644 tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.snap create mode 100644 tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/126_derive_eq_partial/Main.purs create mode 100644 tests-integration/fixtures/checking2/126_derive_eq_partial/Main.snap create mode 100644 tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.snap diff --git a/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.purs b/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.purs new file mode 100644 index 00000000..efdcb3d3 --- /dev/null +++ b/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Eq (class Eq) + +data DurationComponent = Hours | Minutes | Seconds + +data Duration = Duration DurationComponent Int + +derive instance Eq Duration +derive instance Eq DurationComponent diff --git a/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.snap b/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.snap new file mode 100644 index 00000000..d6860998 --- /dev/null +++ b/tests-integration/fixtures/checking2/116_derive_eq_mutual_visibility_same_module/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Hours :: DurationComponent +Minutes :: DurationComponent +Seconds :: DurationComponent +Duration :: DurationComponent -> Int -> Duration + +Types +DurationComponent :: Type +Duration :: Type + +Roles +DurationComponent = [] +Duration = [] diff --git a/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.purs b/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.purs new file mode 100644 index 00000000..64588234 --- /dev/null +++ b/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.purs @@ -0,0 +1,23 @@ +module Main where + +import Data.Newtype (class Newtype, wrap, unwrap) + +newtype UserId = UserId Int + +derive instance Newtype UserId _ + +wrapUserId :: Int -> UserId +wrapUserId = wrap + +unwrapUserId :: UserId -> Int +unwrapUserId = unwrap + +newtype Wrapper a = Wrapper a + +derive instance Newtype (Wrapper a) _ + +wrapWrapper :: forall a. a -> Wrapper a +wrapWrapper = wrap + +unwrapWrapper :: forall a. Wrapper a -> a +unwrapWrapper = unwrap diff --git a/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.snap b/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.snap new file mode 100644 index 00000000..93136293 --- /dev/null +++ b/tests-integration/fixtures/checking2/117_derive_newtype_class_coercible/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +UserId :: Int -> UserId +wrapUserId :: Int -> UserId +unwrapUserId :: UserId -> Int +Wrapper :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) +wrapWrapper :: forall (a :: Type). (a :: Type) -> Wrapper (a :: Type) +unwrapWrapper :: forall (a :: Type). Wrapper (a :: Type) -> (a :: Type) + +Types +UserId :: Type +Wrapper :: Type -> Type + +Roles +UserId = [] +Wrapper = [Representational] diff --git a/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.purs b/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.purs new file mode 100644 index 00000000..45a2f67b --- /dev/null +++ b/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Identity a = Identity a + +derive newtype instance Show a => Show (Identity a) diff --git a/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.snap b/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.snap new file mode 100644 index 00000000..e66717ce --- /dev/null +++ b/tests-integration/fixtures/checking2/118_derive_newtype_with_given/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) + +Types +Identity :: Type -> Type + +Roles +Identity = [Representational] diff --git a/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.purs b/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.purs new file mode 100644 index 00000000..a28cb1bd --- /dev/null +++ b/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Identity a = Identity a + +derive newtype instance Show (Identity String) diff --git a/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.snap b/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.snap new file mode 100644 index 00000000..6714c045 --- /dev/null +++ b/tests-integration/fixtures/checking2/119_derive_newtype_missing_instance/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) + +Types +Identity :: Type -> Type + +Roles +Identity = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.purs b/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.purs new file mode 100644 index 00000000..f99f1ba3 --- /dev/null +++ b/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Identity a = Identity a + +derive newtype instance Show (Identity a) diff --git a/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.snap b/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.snap new file mode 100644 index 00000000..6714c045 --- /dev/null +++ b/tests-integration/fixtures/checking2/120_derive_newtype_missing_given/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Identity :: forall (a :: Type). (a :: Type) -> Identity (a :: Type) + +Types +Identity :: Type -> Type + +Roles +Identity = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.purs b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.purs new file mode 100644 index 00000000..14b8f605 --- /dev/null +++ b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Show (class Show) + +newtype Pair a b = Pair a + +derive newtype instance Show (Pair Int String) diff --git a/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap new file mode 100644 index 00000000..1bc11cdc --- /dev/null +++ b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Pair :: + forall (t2 :: Type) (a :: Type) (b :: (t2 :: Type)). + (a :: Type) -> Pair @(t2 :: Type) (a :: Type) (b :: (t2 :: Type)) + +Types +Pair :: forall (t2 :: Type). Type -> (t2 :: Type) -> Type + +Roles +Pair = [Representational, Phantom] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.purs new file mode 100644 index 00000000..c39d2fd4 --- /dev/null +++ b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.purs @@ -0,0 +1,11 @@ +module Main where + +class Empty f where + empty :: f Int + +instance Empty Array where + empty = [] + +newtype Wrapper a = Wrapper (Array a) + +derive newtype instance Empty Wrapper diff --git a/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap new file mode 100644 index 00000000..4ad40741 --- /dev/null +++ b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int +Wrapper :: forall (a :: Type). Array (a :: Type) -> Wrapper (a :: Type) + +Types +Empty :: (Type -> Type) -> Constraint +Wrapper :: Type -> Type + +Classes +class forall (f :: Type -> Type). Empty (f :: Type -> Type) + empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int + +Instances +instance Empty Array + +Roles +Wrapper = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(6), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.purs b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.purs new file mode 100644 index 00000000..d6a955a8 --- /dev/null +++ b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Control.Category (class Category) +import Data.Semigroupoid (class Semigroupoid) + +newtype Builder a b = Builder (a -> b) + +derive newtype instance Semigroupoid Builder +derive newtype instance Category Builder diff --git a/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap new file mode 100644 index 00000000..06b78edd --- /dev/null +++ b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap @@ -0,0 +1,36 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Builder :: + forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> Builder (a :: Type) (b :: Type) + +Types +Builder :: Type -> Type -> Type + +Roles +Builder = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.purs b/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.purs new file mode 100644 index 00000000..5fc20cd0 --- /dev/null +++ b/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Data.Eq (class Eq) + +foreign import data State :: Type + +instance Eq State where + eq _ _ = true + +newtype Test = Test State + +derive newtype instance Eq Test diff --git a/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.snap b/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.snap new file mode 100644 index 00000000..1e0007ad --- /dev/null +++ b/tests-integration/fixtures/checking2/124_derive_newtype_synonym_inner/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Test :: State -> Test + +Types +State :: Type +Test :: Type + +Instances +instance Eq State + +Roles +State = [] +Test = [] diff --git a/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.purs new file mode 100644 index 00000000..2a449a71 --- /dev/null +++ b/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Data.Eq (class Eq, class Eq1) + +data Wrap f a = MkWrap (f a) + +derive instance (Eq1 f, Eq a) => Eq (Wrap f a) + +data WrapNoEq1 f a = MkWrapNoEq1 (f a) + +derive instance Eq a => Eq (WrapNoEq1 f a) diff --git a/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.snap new file mode 100644 index 00000000..21f9745f --- /dev/null +++ b/tests-integration/fixtures/checking2/125_derive_eq_1_higher_kinded/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +MkWrap :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + Wrap @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) +MkWrapNoEq1 :: + forall (t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + WrapNoEq1 @(t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) + +Types +Wrap :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type +WrapNoEq1 :: forall (t5 :: Type). ((t5 :: Type) -> Type) -> (t5 :: Type) -> Type + +Roles +Wrap = [Representational, Nominal] +WrapNoEq1 = [Representational, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.purs b/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.purs new file mode 100644 index 00000000..e08f6f51 --- /dev/null +++ b/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Eq (class Eq) + +data Either a b = Left a | Right b + +derive instance Eq b => Eq (Either Int b) + +derive instance Eq (Either Int b) diff --git a/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.snap b/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.snap new file mode 100644 index 00000000..eeed192b --- /dev/null +++ b/tests-integration/fixtures/checking2/126_derive_eq_partial/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) + +Types +Either :: Type -> Type -> Type + +Roles +Either = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.purs new file mode 100644 index 00000000..39e57b34 --- /dev/null +++ b/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.purs @@ -0,0 +1,24 @@ +module Main where + +import Data.Eq (class Eq, class Eq1) +import Data.Ord (class Ord, class Ord1) + +data T f g = T (f (g Int)) + +derive instance Eq1 f => Eq (T f g) +derive instance Ord1 f => Ord (T f g) + +data Maybe a = Nothing | Just a + +derive instance Eq a => Eq (Maybe a) +derive instance Ord a => Ord (Maybe a) + +data U f = U (f (Maybe Int)) + +derive instance Eq1 f => Eq (U f) +derive instance Ord1 f => Ord (U f) + +data V f g = V (f (g 42)) + +derive instance Eq1 f => Eq (V f g) +derive instance Ord1 f => Ord (V f g) diff --git a/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.snap new file mode 100644 index 00000000..389d368c --- /dev/null +++ b/tests-integration/fixtures/checking2/127_derive_eq_ord_nested_higher_kinded/Main.snap @@ -0,0 +1,91 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +T :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (g :: Type -> (t2 :: Type)). + (f :: (t2 :: Type) -> Type) ((g :: Type -> (t2 :: Type)) Int) -> + T @(t2 :: Type) (f :: (t2 :: Type) -> Type) (g :: Type -> (t2 :: Type)) +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +U :: forall (f :: Type -> Type). (f :: Type -> Type) (Maybe Int) -> U (f :: Type -> Type) +V :: + forall (t7 :: Type) (f :: (t7 :: Type) -> Type) (g :: Int -> (t7 :: Type)). + (f :: (t7 :: Type) -> Type) ((g :: Int -> (t7 :: Type)) 42) -> + V @(t7 :: Type) (f :: (t7 :: Type) -> Type) (g :: Int -> (t7 :: Type)) + +Types +T :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (Type -> (t2 :: Type)) -> Type +Maybe :: Type -> Type +U :: (Type -> Type) -> Type +V :: forall (t7 :: Type). ((t7 :: Type) -> Type) -> (Int -> (t7 :: Type)) -> Type + +Roles +T = [Representational, Representational] +Maybe = [Representational] +U = [Representational] +V = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(11), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(14), + }, + crumbs: [ + TermDeclaration( + Idx::(12), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.purs new file mode 100644 index 00000000..af0de0ee --- /dev/null +++ b/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Data.Functor (class Functor) + +data Wrap f a = Wrap (f a) +derive instance Functor f => Functor (Wrap f) + +data WrapNoFunctor f a = WrapNoFunctor (f a) +derive instance Functor (WrapNoFunctor f) diff --git a/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.snap new file mode 100644 index 00000000..8acbc7c6 --- /dev/null +++ b/tests-integration/fixtures/checking2/128_derive_functor_higher_kinded/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrap :: + forall (t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)). + (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) -> + Wrap @(t2 :: Type) (f :: (t2 :: Type) -> Type) (a :: (t2 :: Type)) +WrapNoFunctor :: + forall (t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + WrapNoFunctor @(t5 :: Type) (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) + +Types +Wrap :: forall (t2 :: Type). ((t2 :: Type) -> Type) -> (t2 :: Type) -> Type +WrapNoFunctor :: forall (t5 :: Type). ((t5 :: Type) -> Type) -> (t5 :: Type) -> Type + +Roles +Wrap = [Representational, Nominal] +WrapNoFunctor = [Representational, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(6), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.purs b/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.purs new file mode 100644 index 00000000..e94b0c71 --- /dev/null +++ b/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Data.Bifunctor (class Bifunctor) +import Data.Functor (class Functor) + +data WrapBoth f g a b = WrapBoth (f a) (g b) +derive instance (Functor f, Functor g) => Bifunctor (WrapBoth f g) + +data WrapBothNoConstraint f g a b = WrapBothNoConstraint (f a) (g b) +derive instance Bifunctor (WrapBothNoConstraint f g) diff --git a/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.snap new file mode 100644 index 00000000..d8c38686 --- /dev/null +++ b/tests-integration/fixtures/checking2/129_derive_bifunctor_higher_kinded/Main.snap @@ -0,0 +1,80 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +WrapBoth :: + forall (t5 :: Type) (t4 :: Type) (f :: (t5 :: Type) -> Type) (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) (b :: (t4 :: Type)). + (f :: (t5 :: Type) -> Type) (a :: (t5 :: Type)) -> + (g :: (t4 :: Type) -> Type) (b :: (t4 :: Type)) -> + WrapBoth @(t5 :: Type) @(t4 :: Type) + (f :: (t5 :: Type) -> Type) + (g :: (t4 :: Type) -> Type) + (a :: (t5 :: Type)) + (b :: (t4 :: Type)) +WrapBothNoConstraint :: + forall (t11 :: Type) (t10 :: Type) (f :: (t11 :: Type) -> Type) (g :: (t10 :: Type) -> Type) + (a :: (t11 :: Type)) (b :: (t10 :: Type)). + (f :: (t11 :: Type) -> Type) (a :: (t11 :: Type)) -> + (g :: (t10 :: Type) -> Type) (b :: (t10 :: Type)) -> + WrapBothNoConstraint @(t11 :: Type) @(t10 :: Type) + (f :: (t11 :: Type) -> Type) + (g :: (t10 :: Type) -> Type) + (a :: (t11 :: Type)) + (b :: (t10 :: Type)) + +Types +WrapBoth :: + forall (t5 :: Type) (t4 :: Type). + ((t5 :: Type) -> Type) -> ((t4 :: Type) -> Type) -> (t5 :: Type) -> (t4 :: Type) -> Type +WrapBothNoConstraint :: + forall (t11 :: Type) (t10 :: Type). + ((t11 :: Type) -> Type) -> ((t10 :: Type) -> Type) -> (t11 :: Type) -> (t10 :: Type) -> Type + +Roles +WrapBoth = [Representational, Representational, Nominal, Nominal] +WrapBothNoConstraint = [Representational, Representational, Nominal, Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 5e079b86..15e52ead 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -259,3 +259,31 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_114_derive_newtype_class_not_newtype_main() { run_test("114_derive_newtype_class_not_newtype", "Main"); } #[rustfmt::skip] #[test] fn test_115_derive_generic_simple_main() { run_test("115_derive_generic_simple", "Main"); } + +#[rustfmt::skip] #[test] fn test_116_derive_eq_mutual_visibility_same_module_main() { run_test("116_derive_eq_mutual_visibility_same_module", "Main"); } + +#[rustfmt::skip] #[test] fn test_117_derive_newtype_class_coercible_main() { run_test("117_derive_newtype_class_coercible", "Main"); } + +#[rustfmt::skip] #[test] fn test_118_derive_newtype_with_given_main() { run_test("118_derive_newtype_with_given", "Main"); } + +#[rustfmt::skip] #[test] fn test_119_derive_newtype_missing_instance_main() { run_test("119_derive_newtype_missing_instance", "Main"); } + +#[rustfmt::skip] #[test] fn test_120_derive_newtype_missing_given_main() { run_test("120_derive_newtype_missing_given", "Main"); } + +#[rustfmt::skip] #[test] fn test_121_derive_newtype_multi_param_main() { run_test("121_derive_newtype_multi_param", "Main"); } + +#[rustfmt::skip] #[test] fn test_122_derive_newtype_higher_kinded_main() { run_test("122_derive_newtype_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_123_derive_newtype_function_main() { run_test("123_derive_newtype_function", "Main"); } + +#[rustfmt::skip] #[test] fn test_124_derive_newtype_synonym_inner_main() { run_test("124_derive_newtype_synonym_inner", "Main"); } + +#[rustfmt::skip] #[test] fn test_125_derive_eq_1_higher_kinded_main() { run_test("125_derive_eq_1_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_126_derive_eq_partial_main() { run_test("126_derive_eq_partial", "Main"); } + +#[rustfmt::skip] #[test] fn test_127_derive_eq_ord_nested_higher_kinded_main() { run_test("127_derive_eq_ord_nested_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_128_derive_functor_higher_kinded_main() { run_test("128_derive_functor_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_129_derive_bifunctor_higher_kinded_main() { run_test("129_derive_bifunctor_higher_kinded", "Main"); } From 52029d7abe89063552696ae5a5f89a004fb2242f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 02:49:49 +0800 Subject: [PATCH 335/386] Add tests for checking implication scoping --- .../checking2/130_givens_retained/Main.purs | 10 ++++++++ .../checking2/130_givens_retained/Main.snap | 15 ++++++++++++ .../checking2/131_givens_scoped/Main.purs | 13 +++++++++++ .../checking2/131_givens_scoped/Main.snap | 23 +++++++++++++++++++ .../132_let_constraint_scoping/Main.purs | 17 ++++++++++++++ .../132_let_constraint_scoping/Main.snap | 18 +++++++++++++++ .../tests/checking2/generated.rs | 6 +++++ 7 files changed, 102 insertions(+) create mode 100644 tests-integration/fixtures/checking2/130_givens_retained/Main.purs create mode 100644 tests-integration/fixtures/checking2/130_givens_retained/Main.snap create mode 100644 tests-integration/fixtures/checking2/131_givens_scoped/Main.purs create mode 100644 tests-integration/fixtures/checking2/131_givens_scoped/Main.snap create mode 100644 tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.purs create mode 100644 tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.snap diff --git a/tests-integration/fixtures/checking2/130_givens_retained/Main.purs b/tests-integration/fixtures/checking2/130_givens_retained/Main.purs new file mode 100644 index 00000000..43266042 --- /dev/null +++ b/tests-integration/fixtures/checking2/130_givens_retained/Main.purs @@ -0,0 +1,10 @@ +module Main where + +class Given a where + consume :: a -> a + +testGiven :: forall a. Given a => a -> a +testGiven a = consume a + where + b = consume a + c = consume a diff --git a/tests-integration/fixtures/checking2/130_givens_retained/Main.snap b/tests-integration/fixtures/checking2/130_givens_retained/Main.snap new file mode 100644 index 00000000..31c9d400 --- /dev/null +++ b/tests-integration/fixtures/checking2/130_givens_retained/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) +testGiven :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) + +Types +Given :: Type -> Constraint + +Classes +class forall (a :: Type). Given (a :: Type) + consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) diff --git a/tests-integration/fixtures/checking2/131_givens_scoped/Main.purs b/tests-integration/fixtures/checking2/131_givens_scoped/Main.purs new file mode 100644 index 00000000..b14a9d7e --- /dev/null +++ b/tests-integration/fixtures/checking2/131_givens_scoped/Main.purs @@ -0,0 +1,13 @@ +module Main where + +class Given a where + consume :: a -> a + +testGiven :: forall a. Given a => a -> a +testGiven a = consume a + where + b :: Given Int => a + b = let consumeInt = consume 42 in a + + c :: Int + c = consume 42 diff --git a/tests-integration/fixtures/checking2/131_givens_scoped/Main.snap b/tests-integration/fixtures/checking2/131_givens_scoped/Main.snap new file mode 100644 index 00000000..308074be --- /dev/null +++ b/tests-integration/fixtures/checking2/131_givens_scoped/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) +testGiven :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) + +Types +Given :: Type -> Constraint + +Classes +class forall (a :: Type). Given (a :: Type) + consume :: forall (a :: Type). Given (a :: Type) => (a :: Type) -> (a :: Type) + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(6), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.purs b/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.purs new file mode 100644 index 00000000..8a627f95 --- /dev/null +++ b/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.purs @@ -0,0 +1,17 @@ +module Main where + +class MyClass a where + method :: a -> Int + +instance MyClass Int where + method _ = 42 + +test :: forall a. MyClass a => a -> Int +test x = + let + bar y = method y + + baz :: MyClass Int => Int + baz = method 42 + in + bar x diff --git a/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.snap b/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.snap new file mode 100644 index 00000000..84cc19e4 --- /dev/null +++ b/tests-integration/fixtures/checking2/132_let_constraint_scoping/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +method :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int +test :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int + +Types +MyClass :: Type -> Constraint + +Classes +class forall (a :: Type). MyClass (a :: Type) + method :: forall (a :: Type). MyClass (a :: Type) => (a :: Type) -> Int + +Instances +instance MyClass Int diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 15e52ead..a5a5112e 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -287,3 +287,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_128_derive_functor_higher_kinded_main() { run_test("128_derive_functor_higher_kinded", "Main"); } #[rustfmt::skip] #[test] fn test_129_derive_bifunctor_higher_kinded_main() { run_test("129_derive_bifunctor_higher_kinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_130_givens_retained_main() { run_test("130_givens_retained", "Main"); } + +#[rustfmt::skip] #[test] fn test_131_givens_scoped_main() { run_test("131_givens_scoped", "Main"); } + +#[rustfmt::skip] #[test] fn test_132_let_constraint_scoping_main() { run_test("132_let_constraint_scoping", "Main"); } From 6ef60c9948e276c3a38c5df94b23b7d2411cf881 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 02:49:49 +0800 Subject: [PATCH 336/386] Add tests for open rows on compiler-solved classes --- .../checking2/133_row_open_union/Main.purs | 20 ++++++++++ .../checking2/133_row_open_union/Main.snap | 37 +++++++++++++++++++ .../checking2/134_row_open_cons/Main.purs | 17 +++++++++ .../checking2/134_row_open_cons/Main.snap | 31 ++++++++++++++++ .../checking2/135_row_open_lacks/Main.purs | 17 +++++++++ .../checking2/135_row_open_lacks/Main.snap | 30 +++++++++++++++ .../checking2/136_row_open_record/Main.purs | 21 +++++++++++ .../checking2/136_row_open_record/Main.snap | 25 +++++++++++++ .../tests/checking2/generated.rs | 8 ++++ 9 files changed, 206 insertions(+) create mode 100644 tests-integration/fixtures/checking2/133_row_open_union/Main.purs create mode 100644 tests-integration/fixtures/checking2/133_row_open_union/Main.snap create mode 100644 tests-integration/fixtures/checking2/134_row_open_cons/Main.purs create mode 100644 tests-integration/fixtures/checking2/134_row_open_cons/Main.snap create mode 100644 tests-integration/fixtures/checking2/135_row_open_lacks/Main.purs create mode 100644 tests-integration/fixtures/checking2/135_row_open_lacks/Main.snap create mode 100644 tests-integration/fixtures/checking2/136_row_open_record/Main.purs create mode 100644 tests-integration/fixtures/checking2/136_row_open_record/Main.snap diff --git a/tests-integration/fixtures/checking2/133_row_open_union/Main.purs b/tests-integration/fixtures/checking2/133_row_open_union/Main.purs new file mode 100644 index 00000000..ca542ecc --- /dev/null +++ b/tests-integration/fixtures/checking2/133_row_open_union/Main.purs @@ -0,0 +1,20 @@ +module Main where + +import Prim.Row as Row + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +openLeft :: forall r u. Row.Union (a :: Int | r) (b :: String) u => Proxy u +openLeft = Proxy + +openRight :: forall r u. Row.Union (a :: Int) (b :: String | r) u => Proxy u +openRight = Proxy + +backwardLeft :: forall l r. Row.Union l (b :: String) (a :: Int, b :: String | r) => Proxy l +backwardLeft = Proxy + +backwardRight :: forall r u. Row.Union (a :: Int) r (a :: Int, b :: String | u) => Proxy r +backwardRight = Proxy + +forceSolve = { openLeft, openRight, backwardLeft, backwardRight } diff --git a/tests-integration/fixtures/checking2/133_row_open_union/Main.snap b/tests-integration/fixtures/checking2/133_row_open_union/Main.snap new file mode 100644 index 00000000..7bfc5779 --- /dev/null +++ b/tests-integration/fixtures/checking2/133_row_open_union/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +openLeft :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int | (r :: Row Type) ) ( b :: String ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +openRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) ( b :: String | (r :: Row Type) ) (u :: Row Type) => + Proxy @(Row Type) (u :: Row Type) +backwardLeft :: + forall (l :: Row Type) (r :: Row Type). + Union @Type (l :: Row Type) ( b :: String ) ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (l :: Row Type) +backwardRight :: + forall (r :: Row Type) (u :: Row Type). + Union @Type ( a :: Int ) (r :: Row Type) ( a :: Int, b :: String | (u :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) +forceSolve :: + forall (t14 :: Row Type) (t13 :: Row Type) (t12 :: Row Type) (t11 :: Row Type) (t10 :: Row Type). + Union (t14 :: Row Type) ( b :: String ) (t13 :: Row Type) => + { backwardLeft :: Proxy @(Row Type) ( a :: Int | (t12 :: Row Type) ) + , backwardRight :: Proxy @(Row Type) ( b :: String | (t11 :: Row Type) ) + , openLeft :: Proxy @(Row Type) ( a :: Int | (t13 :: Row Type) ) + , openRight :: Proxy @(Row Type) ( a :: Int, b :: String | (t10 :: Row Type) ) + } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/134_row_open_cons/Main.purs b/tests-integration/fixtures/checking2/134_row_open_cons/Main.purs new file mode 100644 index 00000000..6a881ce7 --- /dev/null +++ b/tests-integration/fixtures/checking2/134_row_open_cons/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Prim.Row as Row + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +consOpen :: forall r row. Row.Cons "x" Int (a :: String | r) row => Proxy row +consOpen = Proxy + +decomposeOpen :: forall t tail r. Row.Cons "x" t tail (x :: Int, a :: String | r) => Proxy t +decomposeOpen = Proxy + +extractTail :: forall tail r. Row.Cons "x" Int tail (x :: Int, a :: String | r) => Proxy tail +extractTail = Proxy + +forceSolve = { consOpen, decomposeOpen, extractTail } diff --git a/tests-integration/fixtures/checking2/134_row_open_cons/Main.snap b/tests-integration/fixtures/checking2/134_row_open_cons/Main.snap new file mode 100644 index 00000000..255ec020 --- /dev/null +++ b/tests-integration/fixtures/checking2/134_row_open_cons/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +consOpen :: + forall (r :: Row Type) (row :: Row Type). + Cons @Type "x" Int ( a :: String | (r :: Row Type) ) (row :: Row Type) => + Proxy @(Row Type) (row :: Row Type) +decomposeOpen :: + forall (t :: Type) (tail :: Row Type) (r :: Row Type). + Cons @Type "x" (t :: Type) (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @Type (t :: Type) +extractTail :: + forall (tail :: Row Type) (r :: Row Type). + Cons @Type "x" Int (tail :: Row Type) ( a :: String, x :: Int | (r :: Row Type) ) => + Proxy @(Row Type) (tail :: Row Type) +forceSolve :: + forall (t10 :: Row Type) (t9 :: Row Type). + { consOpen :: Proxy @(Row Type) ( a :: String, x :: Int | (t10 :: Row Type) ) + , decomposeOpen :: Proxy @Type Int + , extractTail :: Proxy @(Row Type) ( a :: String | (t9 :: Row Type) ) + } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/135_row_open_lacks/Main.purs b/tests-integration/fixtures/checking2/135_row_open_lacks/Main.purs new file mode 100644 index 00000000..84c7849c --- /dev/null +++ b/tests-integration/fixtures/checking2/135_row_open_lacks/Main.purs @@ -0,0 +1,17 @@ +module Main where + +import Prim.Row as Row + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +lacksOpen :: forall r. Row.Lacks "missing" (a :: Int, b :: String | r) => Proxy r -> Int +lacksOpen _ = 0 + +lacksPresent :: forall r. Row.Lacks "a" (a :: Int | r) => Proxy r -> Int +lacksPresent _ = 0 + +empty :: Proxy () +empty = Proxy + +forceSolve = { lacksOpen: lacksOpen empty, lacksPresent: lacksPresent empty } diff --git a/tests-integration/fixtures/checking2/135_row_open_lacks/Main.snap b/tests-integration/fixtures/checking2/135_row_open_lacks/Main.snap new file mode 100644 index 00000000..a85a0c11 --- /dev/null +++ b/tests-integration/fixtures/checking2/135_row_open_lacks/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +lacksOpen :: + forall (r :: Row Type). + Lacks @Type "missing" ( a :: Int, b :: String | (r :: Row Type) ) => + Proxy @(Row Type) (r :: Row Type) -> Int +lacksPresent :: + forall (r :: Row Type). + Lacks @Type "a" ( a :: Int | (r :: Row Type) ) => Proxy @(Row Type) (r :: Row Type) -> Int +empty :: forall (t4 :: Type). Proxy @(Row (t4 :: Type)) () +forceSolve :: { lacksOpen :: Int, lacksPresent :: Int } + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type + +Roles +Proxy = [Phantom] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(18), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/136_row_open_record/Main.purs b/tests-integration/fixtures/checking2/136_row_open_record/Main.purs new file mode 100644 index 00000000..9ab0137a --- /dev/null +++ b/tests-integration/fixtures/checking2/136_row_open_record/Main.purs @@ -0,0 +1,21 @@ +module Main where + +import Prim.Row as Row + +foreign import unsafeCoerce :: forall a b. a -> b + +union :: forall r1 r2 r3. Row.Union r1 r2 r3 => Record r1 -> Record r2 -> Record r3 +union _ _ = unsafeCoerce {} + +addField :: forall r. { a :: Int | r } -> { a :: Int, b :: String | r } +addField x = union x { b: "hi" } + +test = addField { a: 1, c: true } + +insertX :: forall r. Row.Lacks "x" r => Record r -> Record (x :: Int | r) +insertX _ = unsafeCoerce {} + +insertOpen :: forall r. Row.Lacks "x" r => { a :: Int | r } -> { x :: Int, a :: Int | r } +insertOpen x = insertX x + +test2 = insertOpen { a: 1, b: "hi" } diff --git a/tests-integration/fixtures/checking2/136_row_open_record/Main.snap b/tests-integration/fixtures/checking2/136_row_open_record/Main.snap new file mode 100644 index 00000000..cd106588 --- /dev/null +++ b/tests-integration/fixtures/checking2/136_row_open_record/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +union :: + forall (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type). + Union @Type (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) => + {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } -> {| (r3 :: Row Type) } +addField :: + forall (r :: Row Type). + { a :: Int | (r :: Row Type) } -> { a :: Int, b :: String | (r :: Row Type) } +test :: { a :: Int, b :: String, c :: Boolean } +insertX :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => {| (r :: Row Type) } -> { x :: Int | (r :: Row Type) } +insertOpen :: + forall (r :: Row Type). + Lacks @Type "x" (r :: Row Type) => + { a :: Int | (r :: Row Type) } -> { a :: Int, x :: Int | (r :: Row Type) } +test2 :: { a :: Int, b :: String, x :: Int } + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index a5a5112e..7982933d 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -293,3 +293,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_131_givens_scoped_main() { run_test("131_givens_scoped", "Main"); } #[rustfmt::skip] #[test] fn test_132_let_constraint_scoping_main() { run_test("132_let_constraint_scoping", "Main"); } + +#[rustfmt::skip] #[test] fn test_133_row_open_union_main() { run_test("133_row_open_union", "Main"); } + +#[rustfmt::skip] #[test] fn test_134_row_open_cons_main() { run_test("134_row_open_cons", "Main"); } + +#[rustfmt::skip] #[test] fn test_135_row_open_lacks_main() { run_test("135_row_open_lacks", "Main"); } + +#[rustfmt::skip] #[test] fn test_136_row_open_record_main() { run_test("136_row_open_record", "Main"); } From 0aa7b6d671286babf09d4a26eb843529c09716fe Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 03:28:52 +0800 Subject: [PATCH 337/386] Format files --- compiler-core/checking2/src/source/derive.rs | 4 +- .../src/source/derive/contravariant.rs | 12 ++-- .../checking2/src/source/derive/eq1_ord1.rs | 5 +- .../checking2/src/source/derive/eq_ord.rs | 3 +- .../checking2/src/source/derive/field.rs | 11 ++-- .../checking2/src/source/derive/foldable.rs | 9 ++- .../checking2/src/source/derive/functor.rs | 9 ++- .../checking2/src/source/derive/generic.rs | 24 ++++---- .../checking2/src/source/derive/head.rs | 60 +++++++++---------- .../src/source/derive/traversable.rs | 9 ++- .../checking2/src/source/derive/variance.rs | 13 ++-- 11 files changed, 86 insertions(+), 73 deletions(-) diff --git a/compiler-core/checking2/src/source/derive.rs b/compiler-core/checking2/src/source/derive.rs index 7707141d..c056b87f 100644 --- a/compiler-core/checking2/src/source/derive.rs +++ b/compiler-core/checking2/src/source/derive.rs @@ -17,11 +17,11 @@ use building_types::QueryResult; use files::FileId; use indexing::{TermItemId, TypeItemId}; -use crate::core::TypeId; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::state::CheckState; +use crate::core::TypeId; use crate::source::derive::variance::VarianceConfig; +use crate::state::CheckState; #[derive(Clone, Copy)] enum DeriveDispatch { diff --git a/compiler-core/checking2/src/source/derive/contravariant.rs b/compiler-core/checking2/src/source/derive/contravariant.rs index 502e80ef..cac30a0f 100644 --- a/compiler-core/checking2/src/source/derive/contravariant.rs +++ b/compiler-core/checking2/src/source/derive/contravariant.rs @@ -31,7 +31,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -69,7 +70,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -78,8 +80,10 @@ where let contravariant = context.known_types.contravariant; let functor = context.known_types.functor; - let config = - VarianceConfig::Pair((Variance::Contravariant, contravariant), (Variance::Covariant, functor)); + let config = VarianceConfig::Pair( + (Variance::Contravariant, contravariant), + (Variance::Covariant, functor), + ); Ok(Some(DeriveStrategy::VarianceConstraints { data_file, diff --git a/compiler-core/checking2/src/source/derive/eq1_ord1.rs b/compiler-core/checking2/src/source/derive/eq1_ord1.rs index d93b036e..7d8fdc56 100644 --- a/compiler-core/checking2/src/source/derive/eq1_ord1.rs +++ b/compiler-core/checking2/src/source/derive/eq1_ord1.rs @@ -73,8 +73,5 @@ where return Ok(None); } - Ok(Some(DeriveStrategy::DelegateConstraint { - derived_type: *derived_type, - class, - })) + Ok(Some(DeriveStrategy::DelegateConstraint { derived_type: *derived_type, class })) } diff --git a/compiler-core/checking2/src/source/derive/eq_ord.rs b/compiler-core/checking2/src/source/derive/eq_ord.rs index a399fa0a..13ba9533 100644 --- a/compiler-core/checking2/src/source/derive/eq_ord.rs +++ b/compiler-core/checking2/src/source/derive/eq_ord.rs @@ -56,7 +56,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs index b7d742ae..99bad668 100644 --- a/compiler-core/checking2/src/source/derive/field.rs +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -33,7 +33,8 @@ where for constructor_id in tools::lookup_data_constructors(context, data_file, data_id)? { let constructor_t = toolkit::lookup_file_term(state, context, data_file, constructor_id)?; - let field_types = instantiate_constructor_fields(state, context, constructor_t, &arguments)?; + let field_types = + instantiate_constructor_fields(state, context, constructor_t, &arguments)?; for field_type in field_types { generate_constraint(state, context, field_type, class, class1)?; } @@ -61,13 +62,13 @@ where }; let binder = context.lookup_forall_binder(binder_id); - let replacement = arguments.next().unwrap_or_else(|| { - state.fresh_rigid(context.queries, binder.kind) - }); + let replacement = + arguments.next().unwrap_or_else(|| state.fresh_rigid(context.queries, binder.kind)); current = SubstituteName::one(state, context, binder.name, replacement, inner)?; } - let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, current)?; + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, current)?; Ok(arguments) } diff --git a/compiler-core/checking2/src/source/derive/foldable.rs b/compiler-core/checking2/src/source/derive/foldable.rs index cac60978..ecb2b506 100644 --- a/compiler-core/checking2/src/source/derive/foldable.rs +++ b/compiler-core/checking2/src/source/derive/foldable.rs @@ -31,7 +31,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -69,7 +70,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -77,7 +79,8 @@ where }; let wrapper = context.known_types.foldable; - let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + let config = + VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); Ok(Some(DeriveStrategy::VarianceConstraints { data_file, diff --git a/compiler-core/checking2/src/source/derive/functor.rs b/compiler-core/checking2/src/source/derive/functor.rs index afec174f..0ffdc742 100644 --- a/compiler-core/checking2/src/source/derive/functor.rs +++ b/compiler-core/checking2/src/source/derive/functor.rs @@ -31,7 +31,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -69,7 +70,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -77,7 +79,8 @@ where }; let wrapper = context.known_types.functor; - let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + let config = + VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); Ok(Some(DeriveStrategy::VarianceConstraints { data_file, diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs index fb74b90b..85854363 100644 --- a/compiler-core/checking2/src/source/derive/generic.rs +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -4,13 +4,12 @@ use indexing::TermItemId; use lowering::StringKind; use smol_str::SmolStr; -use crate::ExternalQueries; use crate::context::{CheckContext, KnownGeneric}; use crate::core::substitute::SubstituteName; use crate::core::{Type, TypeId, normalise, toolkit, unification}; use crate::error::ErrorKind; use crate::state::CheckState; -use crate::safe_loop; +use crate::{ExternalQueries, safe_loop}; use super::{DeriveStrategy, tools}; @@ -34,7 +33,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -74,8 +74,14 @@ where build_generic_constructor(state, context, known_generic, data_file, &arguments, *last)?; for &constructor_id in rest.iter().rev() { - let constructor = - build_generic_constructor(state, context, known_generic, data_file, &arguments, constructor_id)?; + let constructor = build_generic_constructor( + state, + context, + known_generic, + data_file, + &arguments, + constructor_id, + )?; let applied = context.intern_application(known_generic.sum, constructor); rep = context.intern_application(applied, rep); } @@ -100,10 +106,7 @@ where .clone() .unwrap_or_else(|| SmolStr::new_static("")) } else { - context - .queries - .indexed(data_file)? - .items[constructor_id] + context.queries.indexed(data_file)?.items[constructor_id] .name .clone() .unwrap_or_else(|| SmolStr::new_static("")) @@ -184,6 +187,7 @@ where current = SubstituteName::one(state, context, binder.name, argument_type, inner)?; } - let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, current)?; + let toolkit::InspectFunction { arguments, .. } = + toolkit::inspect_function(state, context, current)?; Ok(arguments) } diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index f2827f65..6b6fa1bb 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -11,7 +11,7 @@ use crate::source::types; use crate::state::CheckState; use super::{ - DeriveDispatch, DeriveHeadResult, contravariant, derive_dispatch, eq1_ord1, eq_ord, foldable, + DeriveDispatch, DeriveHeadResult, contravariant, derive_dispatch, eq_ord, eq1_ord1, foldable, functor, generic, newtype, traversable, }; @@ -33,7 +33,13 @@ where return None; }; let resolution = *resolution; - Some(CheckDeriveDeclaration { item_id, newtype: *newtype, constraints, resolution, arguments }) + Some(CheckDeriveDeclaration { + item_id, + newtype: *newtype, + constraints, + resolution, + arguments, + }) }); for item in items { @@ -83,15 +89,19 @@ where }; state.with_error_crumb(ErrorCrumb::TermDeclaration(item_id), |state| { - check_derive_declaration_core(state, context, CheckDeriveDeclarationCore { - derive_id, - item_id, - newtype, - class_file, - class_id, - constraints, - arguments, - }) + check_derive_declaration_core( + state, + context, + CheckDeriveDeclarationCore { + derive_id, + item_id, + newtype, + class_file, + class_id, + constraints, + arguments, + }, + ) }) } @@ -163,22 +173,12 @@ where constraint::instances::validate_rows(state, context, class_file, class_id, &checked_arguments)?; let strategy = if newtype { - newtype::check_derive_newtype( - state, - context, - class_file, - class_id, - &checked_arguments, - )? + newtype::check_derive_newtype(state, context, class_file, class_id, &checked_arguments)? } else { match derive_dispatch(context, class_file, class_id) { - DeriveDispatch::Eq => eq_ord::check_derive_eq( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, + DeriveDispatch::Eq => { + eq_ord::check_derive_eq(state, context, class_file, class_id, &checked_arguments)? + } DeriveDispatch::Eq1 => eq1_ord1::check_derive_eq1( state, context, @@ -256,13 +256,9 @@ where class_id, &checked_arguments, )?, - DeriveDispatch::Ord => eq_ord::check_derive_ord( - state, - context, - class_file, - class_id, - &checked_arguments, - )?, + DeriveDispatch::Ord => { + eq_ord::check_derive_ord(state, context, class_file, class_id, &checked_arguments)? + } DeriveDispatch::Ord1 => eq1_ord1::check_derive_ord1( state, context, diff --git a/compiler-core/checking2/src/source/derive/traversable.rs b/compiler-core/checking2/src/source/derive/traversable.rs index fa5ddb47..606d4491 100644 --- a/compiler-core/checking2/src/source/derive/traversable.rs +++ b/compiler-core/checking2/src/source/derive/traversable.rs @@ -31,7 +31,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -69,7 +70,8 @@ where return Ok(None); }; - let Some((data_file, data_id)) = toolkit::extract_type_constructor(state, context, *derived_type)? + let Some((data_file, data_id)) = + toolkit::extract_type_constructor(state, context, *derived_type)? else { let type_message = state.pretty_id(context, *derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); @@ -77,7 +79,8 @@ where }; let wrapper = context.known_types.traversable; - let config = VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); + let config = + VarianceConfig::Pair((Variance::Covariant, wrapper), (Variance::Covariant, wrapper)); Ok(Some(DeriveStrategy::VarianceConstraints { data_file, diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 3fcdb3a4..0042c87c 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -123,12 +123,13 @@ where (VarianceConfig::Single((expected, class)), [.., a]) => { DerivedRigids::Single(DerivedParameter { name: *a, expected, class }) } - (VarianceConfig::Pair((first_expected, first_class), (second_expected, second_class)), [.., a, b]) => { - DerivedRigids::Pair( - DerivedParameter { name: *a, expected: first_expected, class: first_class }, - DerivedParameter { name: *b, expected: second_expected, class: second_class }, - ) - } + ( + VarianceConfig::Pair((first_expected, first_class), (second_expected, second_class)), + [.., a, b], + ) => DerivedRigids::Pair( + DerivedParameter { name: *a, expected: first_expected, class: first_class }, + DerivedParameter { name: *b, expected: second_expected, class: second_class }, + ), _ => { let type_message = state.pretty_id(context, derived_type)?; state.insert_error(ErrorKind::CannotDeriveForType { type_message }); From 17dc5f08eb20d1cc460ecc7a437e0bc9492fa4b6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 22:02:47 +0800 Subject: [PATCH 338/386] Remove OperatorApplication and OperatorConstructor from core --- compiler-core/checking2/src/context.rs | 11 -- compiler-core/checking2/src/core.rs | 4 - .../core/constraint/compiler/prim_coerce.rs | 6 -- .../checking2/src/core/constraint/given.rs | 7 -- .../src/core/constraint/instances.rs | 7 -- compiler-core/checking2/src/core/fold.rs | 7 +- .../checking2/src/core/generalise.rs | 5 - compiler-core/checking2/src/core/pretty.rs | 19 ---- compiler-core/checking2/src/core/toolkit.rs | 24 +++++ .../checking2/src/core/unification.rs | 29 +---- compiler-core/checking2/src/core/walk.rs | 6 +- .../checking2/src/source/derive/variance.rs | 4 - .../checking2/src/source/operator.rs | 100 +++++++++++++----- compiler-core/checking2/src/source/roles.rs | 9 +- compiler-core/checking2/src/source/types.rs | 16 ++- .../016_type_operator_chain_infer/Main.snap | 8 +- .../017_type_operator_chain_check/Main.snap | 4 +- .../Main.snap | 4 +- .../Main.snap | 8 +- .../Main.snap | 2 +- .../Main.purs | 12 +++ .../Main.snap | 24 +++++ .../tests/checking2/generated.rs | 2 + 23 files changed, 166 insertions(+), 152 deletions(-) create mode 100644 tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs create mode 100644 tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap diff --git a/compiler-core/checking2/src/context.rs b/compiler-core/checking2/src/context.rs index 3e8f9694..1ed66916 100644 --- a/compiler-core/checking2/src/context.rs +++ b/compiler-core/checking2/src/context.rs @@ -136,17 +136,6 @@ where self.queries.intern_type(Type::KindApplication(function, argument)) } - /// Interns a [`Type::OperatorApplication`] node. - pub fn intern_operator_application( - &self, - file_id: FileId, - type_id: TypeItemId, - left: TypeId, - right: TypeId, - ) -> TypeId { - self.queries.intern_type(Type::OperatorApplication(file_id, type_id, left, right)) - } - /// Interns a [`Type::SynonymApplication`] node. pub fn intern_synonym_application(&self, synonym_id: SynonymId) -> TypeId { self.queries.intern_type(Type::SynonymApplication(synonym_id)) diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 826e6b6f..60133f33 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -152,8 +152,6 @@ pub enum Type { Application(TypeId, TypeId), /// Kind application, `Proxy @Int`. KindApplication(TypeId, TypeId), - /// Binary type operator application, `Fields + ()`. - OperatorApplication(FileId, TypeItemId, TypeId, TypeId), /// Type synonym application, see [`Synonym`]. SynonymApplication(SynonymId), @@ -168,8 +166,6 @@ pub enum Type { /// A resolved type constructor reference. Constructor(FileId, TypeItemId), - /// A resolved type operator reference. - OperatorConstructor(FileId, TypeItemId), /// A type-level integer literal, `42`. Integer(i32), diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index d2a12085..3bfc3920 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -418,12 +418,6 @@ where Ok(try_refl(state, context, b1.kind, b2.kind)? && try_refl(state, context, i1, i2)?) } - (Type::OperatorApplication(f1, i1, l1, r1), Type::OperatorApplication(f2, i2, l2, r2)) => { - Ok((f1, i1) == (f2, i2) - && try_refl(state, context, l1, l2)? - && try_refl(state, context, r1, r2)?) - } - (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { let s1 = context.lookup_synonym(s1); let s2 = context.lookup_synonym(s2); diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index 7f34e0f9..c5bfce2a 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -208,13 +208,6 @@ where .and_then(|| match_given_type(state, context, wa, ga)) } - (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) - if wf == gf && wi == gi => - { - match_given_type(state, context, wl, gl)? - .and_then(|| match_given_type(state, context, wr, gr)) - } - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { let wsyn = context.lookup_synonym(wsyn); let gsyn = context.lookup_synonym(gsyn); diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index 8ea56f08..8f6f5bcf 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -343,13 +343,6 @@ where .and_then(|| match_type(state, context, bindings, equalities, wk, gk)) } - (Type::OperatorApplication(wf, wi, wl, wr), Type::OperatorApplication(gf, gi, gl, gr)) - if wf == gf && wi == gi => - { - match_type(state, context, bindings, equalities, wl, gl)? - .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) - } - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { let wsyn = context.lookup_synonym(wsyn); let gsyn = context.lookup_synonym(gsyn); diff --git a/compiler-core/checking2/src/core/fold.rs b/compiler-core/checking2/src/core/fold.rs index a594e0b9..194314ce 100644 --- a/compiler-core/checking2/src/core/fold.rs +++ b/compiler-core/checking2/src/core/fold.rs @@ -70,11 +70,6 @@ where let argument = fold_type(state, context, argument, folder)?; context.intern_kind_application(function, argument) } - Type::OperatorApplication(file_id, type_id, left, right) => { - let left = fold_type(state, context, left, folder)?; - let right = fold_type(state, context, right, folder)?; - context.intern_operator_application(file_id, type_id, left, right) - } Type::SynonymApplication(synonym_id) => { let mut synonym = context.lookup_synonym(synonym_id); synonym.arguments = synonym @@ -108,7 +103,7 @@ where let kind = fold_type(state, context, kind, folder)?; context.intern_kinded(inner, kind) } - Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => id, + Type::Constructor(_, _) => id, Type::Integer(_) | Type::String(_, _) => id, Type::Row(row_id) => { let mut row = context.lookup_row_type(row_id); diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 34329f2f..b5918eb6 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -60,10 +60,6 @@ where aux(graph, state, context, function, dependent, visited_kinds)?; aux(graph, state, context, argument, dependent, visited_kinds)?; } - Type::OperatorApplication(_, _, left, right) => { - aux(graph, state, context, left, dependent, visited_kinds)?; - aux(graph, state, context, right, dependent, visited_kinds)?; - } Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); for &argument in synonym.arguments.iter() { @@ -112,7 +108,6 @@ where } } Type::Constructor(_, _) - | Type::OperatorConstructor(_, _) | Type::Integer(_) | Type::String(_, _) | Type::Free(_) diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 514c4ce7..88e508b1 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -143,18 +143,6 @@ where self.traverse_kind_application(precedence, function, argument) } - Type::OperatorApplication(file_id, type_id, left, right) => { - let operator = self - .lookup_type_name(file_id, type_id) - .unwrap_or_else(|| "".to_string()); - - let left = self.traverse(Precedence::Application, left); - let right = self.traverse(Precedence::Application, right); - - let operator = left.append(self.arena.text(format!(" {operator} "))).append(right); - self.parens_if(precedence > Precedence::Application, operator) - } - Type::SynonymApplication(synonym_id) => { let synonym = self.lookup_synonym(synonym_id); let (file_id, type_id) = synonym.reference; @@ -207,13 +195,6 @@ where self.arena.text(name) } - Type::OperatorConstructor(file_id, type_id) => { - let name = self - .lookup_type_name(file_id, type_id) - .unwrap_or_else(|| "".to_string()); - self.arena.text(name) - } - Type::Integer(integer) => { let negative = integer.is_negative(); let integer = self.arena.text(format!("{integer}")); diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 8da80b11..15d53cb2 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use building_types::QueryResult; use files::FileId; use indexing::{TermItemId, TypeItemId}; +use lowering::TypeItemIr; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; @@ -184,6 +185,29 @@ where } } +pub fn resolve_type_operator_target( + context: &CheckContext, + file_id: FileId, + type_id: TypeItemId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let resolve = |lowered: &lowering::LoweredModule| { + lowered.info.get_type_item(type_id).and_then(|item| match item { + TypeItemIr::Operator { resolution, .. } => *resolution, + _ => None, + }) + }; + + if file_id == context.id { + Ok(resolve(&context.lowered)) + } else { + let lowered = context.queries.lowered(file_id)?; + Ok(resolve(&lowered)) + } +} + pub fn lookup_file_term_operator( state: &CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 4967406d..6b848940 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -268,13 +268,6 @@ where && unify(state, context, t1_argument, t2_argument)? } - ( - Type::OperatorApplication(t1_file, t1_type, t1_left, t1_right), - Type::OperatorApplication(t2_file, t2_type, t2_left, t2_right), - ) if t1_file == t2_file && t1_type == t2_type => { - unify(state, context, t1_left, t2_left)? && unify(state, context, t1_right, t2_right)? - } - // Forall-Forall // // forall a. A ~ forall b. B @@ -348,11 +341,6 @@ where true } - ( - Type::OperatorConstructor(t1_file, t1_item), - Type::OperatorConstructor(t2_file, t2_item), - ) if t1_file == t2_file && t1_item == t2_item => true, - (Type::Integer(t1_value), Type::Integer(t2_value)) if t1_value == t2_value => true, (Type::String(t1_kind, t1_value), Type::String(t2_kind, t2_value)) @@ -461,18 +449,6 @@ where .and_then(|| can_unify(state, context, t1_inner, t2_inner)) } - ( - Type::OperatorApplication(t1_file, t1_item, t1_left, t1_right), - Type::OperatorApplication(t2_file, t2_item, t2_left, t2_right), - ) => { - if t1_file == t2_file && t1_item == t2_item { - can_unify(state, context, t1_left, t2_left)? - .and_then(|| can_unify(state, context, t1_right, t2_right)) - } else { - Ok(CanUnify::Apart) - } - } - (Type::SynonymApplication(t1_synonym), Type::SynonymApplication(t2_synonym)) => { let t1_synonym = context.lookup_synonym(t1_synonym); let t2_synonym = context.lookup_synonym(t2_synonym); @@ -592,9 +568,6 @@ where .and_then(|| check(promote, state, context, argument)) } - Type::OperatorApplication(_, _, left, right) => check(promote, state, context, left)? - .and_then(|| check(promote, state, context, right)), - Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); for &argument in synonym.arguments.iter() { @@ -630,7 +603,7 @@ where Type::Kinded(inner, kind) => check(promote, state, context, inner)? .and_then(|| check(promote, state, context, kind)), - Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => Ok(PromoteResult::Ok), + Type::Constructor(_, _) => Ok(PromoteResult::Ok), Type::Integer(_) | Type::String(_, _) => Ok(PromoteResult::Ok), Type::Row(row_id) => { diff --git a/compiler-core/checking2/src/core/walk.rs b/compiler-core/checking2/src/core/walk.rs index 65765d27..d52d1078 100644 --- a/compiler-core/checking2/src/core/walk.rs +++ b/compiler-core/checking2/src/core/walk.rs @@ -47,10 +47,6 @@ where walk_type(state, context, function, walker)?; walk_type(state, context, argument, walker)?; } - Type::OperatorApplication(_, _, left, right) => { - walk_type(state, context, left, walker)?; - walk_type(state, context, right, walker)?; - } Type::SynonymApplication(synonym_id) => { let synonym = context.queries.lookup_synonym(synonym_id); for &argument in synonym.arguments.iter() { @@ -75,7 +71,7 @@ where walk_type(state, context, inner, walker)?; walk_type(state, context, kind, walker)?; } - Type::Constructor(_, _) | Type::OperatorConstructor(_, _) => {} + Type::Constructor(_, _) => {} Type::Integer(_) | Type::String(_, _) => {} Type::Row(row_id) => { let row = context.queries.lookup_row_type(row_id); diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 0042c87c..18ee930c 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -242,10 +242,6 @@ where contains_rigid_name(state, context, function, name)? || contains_rigid_name(state, context, argument, name)? } - Type::OperatorApplication(_, _, left, right) => { - contains_rigid_name(state, context, left, name)? - || contains_rigid_name(state, context, right, name)? - } Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); let mut contains = false; diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 46e30ad3..10baeda6 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -2,13 +2,15 @@ use building_types::QueryResult; use files::FileId; -use indexing::{TermItemId, TypeItemId}; +use indexing::TermItemId; use lowering::IsElement; use sugar::OperatorTree; use sugar::bracketing::BracketingResult; use crate::context::CheckContext; -use crate::core::{TypeId, normalise, toolkit, unification}; +use crate::core::substitute::SubstituteName; +use crate::core::{Type, TypeId, normalise, toolkit, unification}; +use crate::error::ErrorKind; use crate::source::{binder, terms, types}; use crate::state::CheckState; use crate::{ExternalQueries, OperatorBranchTypes}; @@ -159,27 +161,7 @@ where OperatorKindMode::Check { expected_type: right_type }, )?; - E::build(state, context, operator, (left, right), result_type) -} - -pub fn elaborate_operator_application_kind( - state: &mut CheckState, - context: &CheckContext, - file_id: FileId, - type_id: TypeItemId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let operator_kind = toolkit::lookup_file_type(state, context, file_id, type_id)?; - let operator_kind = toolkit::instantiate_unifications(state, context, operator_kind)?; - let operator_function = toolkit::inspect_function(state, context, operator_kind)?; - - if operator_function.arguments.len() >= 2 { - Ok(operator_function.result) - } else { - Ok(context.unknown("invalid operator kind")) - } + E::build(state, context, operator, (left, right), (left_type, right_type), result_type) } pub trait IsOperator: IsElement { @@ -223,6 +205,7 @@ pub trait IsOperator: IsElement { context: &CheckContext, operator: (FileId, Self::ItemId), result_tree: (Self::Elaborated, Self::Elaborated), + argument_types: (TypeId, TypeId), result_type: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)>; @@ -288,6 +271,7 @@ impl IsOperator for lowering::ExpressionId { _context: &CheckContext, (_, _): (FileId, Self::ItemId), (_, _): (Self::Elaborated, Self::Elaborated), + (_, _): (TypeId, TypeId), result_type: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)> { Ok(((), result_type)) @@ -361,9 +345,22 @@ impl IsOperator for lowering::TypeId { context: &CheckContext, (file_id, item_id): (FileId, Self::ItemId), (left, right): (Self::Elaborated, Self::Elaborated), + (left_kind, right_kind): (TypeId, TypeId), result_kind: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)> { - let elaborated_type = context.intern_operator_application(file_id, item_id, left, right); + let Some((target_file_id, target_item_id)) = + toolkit::resolve_type_operator_target(context, file_id, item_id)? + else { + let unknown = context.unknown("missing operator resolution"); + return Ok((unknown, unknown)); + }; + + let operator_kind = toolkit::lookup_file_type(state, context, file_id, item_id)?; + let function_type = context.queries.intern_type(Type::Constructor(target_file_id, target_item_id)); + let (function_type, function_kind) = + apply_type_argument(state, context, (function_type, operator_kind), left, left_kind)?; + let (elaborated_type, _) = + apply_type_argument(state, context, (function_type, function_kind), right, right_kind)?; let result_kind = normalise::normalise(state, context, result_kind)?; Ok((elaborated_type, result_kind)) } @@ -436,6 +433,7 @@ impl IsOperator for lowering::BinderId { _context: &CheckContext, (_, _): (FileId, Self::ItemId), (_, _): (Self::Elaborated, Self::Elaborated), + (_, _): (TypeId, TypeId), result_type: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)> { Ok(((), result_type)) @@ -455,3 +453,57 @@ impl IsOperator for lowering::BinderId { .insert(operator_id, OperatorBranchTypes { left, right, result }); } } + +fn apply_type_argument( + state: &mut CheckState, + context: &CheckContext, + (mut function_type, mut function_kind): (TypeId, TypeId), + argument_type: TypeId, + argument_kind: TypeId, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + loop { + function_kind = normalise::normalise(state, context, function_kind)?; + + match context.lookup_type(function_kind) { + Type::Function(expected_kind, result_kind) => { + unification::subtype(state, context, argument_kind, expected_kind)?; + let result_type = context.intern_application(function_type, argument_type); + let result_kind = normalise::normalise(state, context, result_kind)?; + return Ok((result_type, result_kind)); + } + + Type::Unification(unification_id) => { + let result_u = state.fresh_unification(context.queries, context.prim.t); + let function_u = context.intern_function(argument_kind, result_u); + unification::solve(state, context, function_kind, unification_id, function_u)?; + function_kind = result_u; + } + + Type::Forall(binder_id, inner_kind) => { + let binder = context.lookup_forall_binder(binder_id); + let binder_kind = normalise::normalise(state, context, binder.kind)?; + let kind_argument = state.fresh_unification(context.queries, binder_kind); + function_type = context.intern_kind_application(function_type, kind_argument); + function_kind = + SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; + } + + _ => { + let invalid = context.intern_application(function_type, argument_type); + let function_type_message = state.pretty_id(context, function_type)?; + let function_kind_message = state.pretty_id(context, function_kind)?; + let argument_type_message = state.pretty_id(context, argument_type)?; + state.insert_error(ErrorKind::InvalidTypeApplication { + function_type: function_type_message, + function_kind: function_kind_message, + argument_type: argument_type_message, + }); + let unknown = context.unknown("cannot apply operator type"); + return Ok((invalid, unknown)); + } + } + } +} diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index 0afe5073..f3b3e2a3 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -213,11 +213,6 @@ where infer_roles(inference, kind, mode.child())?; } - Type::OperatorApplication(_, _, left, right) => { - infer_roles(inference, left, mode.child())?; - infer_roles(inference, right, mode.child())?; - } - Type::Row(row_id) => { let row = inference.context.lookup_row_type(row_id); @@ -237,9 +232,7 @@ where } } - Type::Constructor(_, _) - | Type::OperatorConstructor(_, _) - | Type::Integer(_) + Type::Constructor(_, _) | Type::Integer(_) | Type::String(_, _) | Type::Unification(_) | Type::Free(_) diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 1e35605a..2a08fbd3 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -215,7 +215,13 @@ where return Ok(unknown("missing operator")); }; - let t = context.queries.intern_type(Type::OperatorConstructor(file_id, type_id)); + let Some((file_id, type_id)) = + toolkit::resolve_type_operator_target(context, file_id, type_id)? + else { + return Ok(unknown("missing operator")); + }; + + let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); let k = toolkit::lookup_file_type(state, context, file_id, type_id)?; Ok((t, k)) @@ -545,14 +551,6 @@ where toolkit::lookup_file_type(state, context, file_id, type_id)? } - Type::OperatorConstructor(file_id, type_id) => { - toolkit::lookup_file_type(state, context, file_id, type_id)? - } - - Type::OperatorApplication(file_id, type_id, _, _) => { - operator::elaborate_operator_application_kind(state, context, file_id, type_id)? - } - Type::Integer(_) => context.prim.int, Type::String(_, _) => context.prim.symbol, Type::Kinded(_, kind) => kind, diff --git a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap index 25dd4865..068ea780 100644 --- a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap +++ b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap @@ -17,5 +17,9 @@ ChainParen :: Synonyms type Add a b = (a :: (t3 :: Type)) -type Chain a b c = (a :: (t9 :: Type)) + (b :: (t8 :: Type)) + (c :: (t7 :: Type)) -type ChainParen a b c = (a :: (t15 :: Type)) + (b :: (t14 :: Type)) + (c :: (t13 :: Type)) +type Chain a b c = Add @(t9 :: Type) @(t7 :: Type) + (Add @(t9 :: Type) @(t8 :: Type) (a :: (t9 :: Type)) (b :: (t8 :: Type))) + (c :: (t7 :: Type)) +type ChainParen a b c = Add @(t15 :: Type) @(t13 :: Type) + (Add @(t15 :: Type) @(t14 :: Type) (a :: (t15 :: Type)) (b :: (t14 :: Type))) + (c :: (t13 :: Type)) diff --git a/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap index 55779add..3e1a8025 100644 --- a/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap +++ b/tests-integration/fixtures/checking2/017_type_operator_chain_check/Main.snap @@ -13,5 +13,5 @@ ChainParen :: Type -> Type -> Type -> Type Synonyms type Add a b = (a :: Type) -type Chain a b c = (a :: Type) + (b :: Type) + (c :: Type) -type ChainParen a b c = (a :: Type) + (b :: Type) + (c :: Type) +type Chain a b c = Add (Add (a :: Type) (b :: Type)) (c :: Type) +type ChainParen a b c = Add (Add (a :: Type) (b :: Type)) (c :: Type) diff --git a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap index 50181c3e..8c7cf575 100644 --- a/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap +++ b/tests-integration/fixtures/checking2/018_type_operator_chain_polykind/Main.snap @@ -13,5 +13,5 @@ ChainParen :: forall (k :: Type). (k :: Type) -> (k :: Type) -> (k :: Type) -> ( Synonyms type Add a b = (a :: (k :: Type)) -type Chain a b c = (a :: (k :: Type)) + (b :: (k :: Type)) + (c :: (k :: Type)) -type ChainParen a b c = (a :: (k :: Type)) + (b :: (k :: Type)) + (c :: (k :: Type)) +type Chain a b c = Add @(k :: Type) (Add @(k :: Type) (a :: (k :: Type)) (b :: (k :: Type))) (c :: (k :: Type)) +type ChainParen a b c = Add @(k :: Type) (Add @(k :: Type) (a :: (k :: Type)) (b :: (k :: Type))) (c :: (k :: Type)) diff --git a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap index 3033329d..81f00a18 100644 --- a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap +++ b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap @@ -20,5 +20,9 @@ ChainParen :: Synonyms type Add a b = (a :: (t3 :: Type)) type Mul a b = (a :: (t7 :: Type)) -type Chain a b c = (a :: (t13 :: Type)) :+: (b :: (t12 :: Type)) :*: (c :: (t11 :: Type)) -type ChainParen a b c = (a :: (t19 :: Type)) :+: (b :: (t18 :: Type)) :*: (c :: (t17 :: Type)) +type Chain a b c = Add @(t13 :: Type) @(t12 :: Type) + (a :: (t13 :: Type)) + (Mul @(t12 :: Type) @(t11 :: Type) (b :: (t12 :: Type)) (c :: (t11 :: Type))) +type ChainParen a b c = Mul @(t19 :: Type) @(t17 :: Type) + (Add @(t19 :: Type) @(t18 :: Type) (a :: (t19 :: Type)) (b :: (t18 :: Type))) + (c :: (t17 :: Type)) diff --git a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap index 9110e3e4..06c09f03 100644 --- a/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap +++ b/tests-integration/fixtures/checking2/020_type_operator_chain_kind_error/Main.snap @@ -12,7 +12,7 @@ Bad :: Type Synonyms type Add a b = (a :: Type) -type Bad = Int + "hello" +type Bad = Add Int "hello" Errors CheckError { diff --git a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs new file mode 100644 index 00000000..7f91ec98 --- /dev/null +++ b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs @@ -0,0 +1,12 @@ +module Main where + +data Maybe a = Just a | Nothing + +type NaturalTransformation :: forall k. (k -> Type) -> (k -> Type) -> Type +type NaturalTransformation f g = forall a. f a -> g a + +infixr 4 type NaturalTransformation as ~> + +type Desugared = NaturalTransformation Array Maybe + +type Operator = Array ~> Maybe diff --git a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap new file mode 100644 index 00000000..6fa1fb7d --- /dev/null +++ b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) + +Types +Maybe :: Type -> Type +NaturalTransformation :: forall (k :: Type). ((k :: Type) -> Type) -> ((k :: Type) -> Type) -> Type +~> :: forall (k :: Type). ((k :: Type) -> Type) -> ((k :: Type) -> Type) -> Type +Desugared :: Type +Operator :: Type + +Synonyms +type NaturalTransformation f g = forall (a :: (k :: Type)). + (f :: (k :: Type) -> Type) (a :: (k :: Type)) -> (g :: (k :: Type) -> Type) (a :: (k :: Type)) +type Desugared = NaturalTransformation Array Maybe +type Operator = NaturalTransformation @Type Array Maybe + +Roles +Maybe = [Representational] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 7982933d..1e3b570c 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -301,3 +301,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_135_row_open_lacks_main() { run_test("135_row_open_lacks", "Main"); } #[rustfmt::skip] #[test] fn test_136_row_open_record_main() { run_test("136_row_open_record", "Main"); } + +#[rustfmt::skip] #[test] fn test_137_type_operator_synonym_expansion_main() { run_test("137_type_operator_synonym_expansion", "Main"); } From ff4ab62125d905c298ff0a3b08ee4f71759feab7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 4 Mar 2026 23:20:54 +0800 Subject: [PATCH 339/386] Refactor synonyms to have kind applications --- compiler-core/checking2/src/core.rs | 10 +++- .../core/constraint/compiler/prim_coerce.rs | 13 +++-- .../checking2/src/core/constraint/given.rs | 20 +++++--- .../src/core/constraint/instances.rs | 14 ++++-- compiler-core/checking2/src/core/fold.rs | 11 ++++- .../checking2/src/core/generalise.rs | 7 ++- compiler-core/checking2/src/core/pretty.rs | 11 +++-- .../checking2/src/core/unification.rs | 23 ++++++--- compiler-core/checking2/src/core/walk.rs | 7 ++- .../checking2/src/source/derive/variance.rs | 7 ++- .../checking2/src/source/operator.rs | 3 +- compiler-core/checking2/src/source/roles.rs | 10 ++-- compiler-core/checking2/src/source/synonym.rs | 49 ++++++++++++------- compiler-core/checking2/src/source/types.rs | 22 +++++++-- .../Main.purs | 5 +- .../Main.snap | 2 +- .../138_synonym_kind_application/Main.purs | 9 ++++ .../138_synonym_kind_application/Main.snap | 18 +++++++ .../139_synonym_operator_alias/Main.purs | 9 ++++ .../139_synonym_operator_alias/Main.snap | 30 ++++++++++++ .../tests/checking2/generated.rs | 4 ++ 21 files changed, 222 insertions(+), 62 deletions(-) create mode 100644 tests-integration/fixtures/checking2/138_synonym_kind_application/Main.purs create mode 100644 tests-integration/fixtures/checking2/138_synonym_kind_application/Main.snap create mode 100644 tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.purs create mode 100644 tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 60133f33..8ecb0d75 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -92,6 +92,12 @@ pub enum Saturation { Partial, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KindOrType { + Kind(TypeId), + Type(TypeId), +} + /// Represents a type synonym. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Synonym { @@ -99,8 +105,8 @@ pub struct Synonym { pub saturation: Saturation, /// The reference to the synonym type. pub reference: (FileId, TypeItemId), - /// Arguments to the synonym constructor. - pub arguments: Arc<[TypeId]>, + /// Kind and type arguments to the synonym constructor. + pub arguments: Arc<[KindOrType]>, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index 3bfc3920..4b3a0c43 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -9,7 +9,7 @@ use crate::context::CheckContext; use crate::core::constraint::MatchInstance; use crate::core::substitute::SubstituteName; use crate::core::unification::{CanUnify, can_unify}; -use crate::core::{Role, Type, TypeId, normalise, toolkit}; +use crate::core::{KindOrType, Role, Type, TypeId, normalise, toolkit}; use crate::error::ErrorKind; use crate::source::types; use crate::state::CheckState; @@ -426,8 +426,15 @@ where } let s1_args = Arc::clone(&s1.arguments); let s2_args = Arc::clone(&s2.arguments); - for (&a1, &a2) in s1_args.iter().zip(s2_args.iter()) { - if !try_refl(state, context, a1, a2)? { + for (a1, a2) in s1_args.iter().zip(s2_args.iter()) { + let equivalent = match (a1, a2) { + (KindOrType::Kind(a1), KindOrType::Kind(a2)) + | (KindOrType::Type(a1), KindOrType::Type(a2)) => { + try_refl(state, context, *a1, *a2)? + } + _ => false, + }; + if !equivalent { return Ok(false); } } diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index c5bfce2a..2d4b16b0 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -3,7 +3,7 @@ use itertools::Itertools; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{Type, TypeId, normalise}; +use crate::core::{KindOrType, Type, TypeId, normalise}; use crate::state::CheckState; use super::{ @@ -216,12 +216,18 @@ where return Ok(MatchType::Apart); } - wsyn.arguments - .iter() - .zip(gsyn.arguments.iter()) - .try_fold(MatchType::Match, |result, (&wa, &ga)| { - result.and_then(|| match_given_type(state, context, wa, ga)) - }) + wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( + MatchType::Match, + |result, (wa, ga)| { + result.and_then(|| match (wa, ga) { + (KindOrType::Kind(wa), KindOrType::Kind(ga)) + | (KindOrType::Type(wa), KindOrType::Type(ga)) => { + match_given_type(state, context, *wa, *ga) + } + _ => Ok(MatchType::Apart), + }) + }, + ) } _ => Ok(MatchType::Apart), diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index 8f6f5bcf..71e1ebf3 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -10,7 +10,9 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::unification::{CanUnify, can_unify}; use crate::core::walk::{TypeWalker, WalkAction, walk_type}; -use crate::core::{CheckedInstance, Name, RowField, RowType, Type, TypeId, normalise, toolkit}; +use crate::core::{ + CheckedInstance, KindOrType, Name, RowField, RowType, Type, TypeId, normalise, toolkit, +}; use crate::error::ErrorKind; use crate::state::CheckState; use crate::{CheckedModule, ExternalQueries}; @@ -353,8 +355,14 @@ where wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( MatchType::Match, - |result, (&wa, &ga)| { - result.and_then(|| match_type(state, context, bindings, equalities, wa, ga)) + |result, (wa, ga)| { + result.and_then(|| match (wa, ga) { + (KindOrType::Kind(wa), KindOrType::Kind(ga)) + | (KindOrType::Type(wa), KindOrType::Type(ga)) => { + match_type(state, context, bindings, equalities, *wa, *ga) + } + _ => Ok(MatchType::Apart), + }) }, ) } diff --git a/compiler-core/checking2/src/core/fold.rs b/compiler-core/checking2/src/core/fold.rs index 194314ce..5b9c91aa 100644 --- a/compiler-core/checking2/src/core/fold.rs +++ b/compiler-core/checking2/src/core/fold.rs @@ -6,7 +6,7 @@ use building_types::QueryResult; use crate::context::CheckContext; use crate::core::normalise::normalise; -use crate::core::{ForallBinder, Type, TypeId}; +use crate::core::{ForallBinder, KindOrType, Type, TypeId}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -75,7 +75,14 @@ where synonym.arguments = synonym .arguments .iter() - .map(|&argument| fold_type(state, context, argument, folder)) + .map(|application| match application { + KindOrType::Kind(argument) => { + fold_type(state, context, *argument, folder).map(KindOrType::Kind) + } + KindOrType::Type(argument) => { + fold_type(state, context, *argument, folder).map(KindOrType::Type) + } + }) .collect::>()?; let synonym_id = context.intern_synonym(synonym); context.intern_synonym_application(synonym_id) diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index b5918eb6..4cd4b3b1 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -26,7 +26,7 @@ use rustc_hash::FxHashSet; use crate::context::CheckContext; use crate::core::walk::{TypeWalker, WalkAction, walk_type}; -use crate::core::{ForallBinder, Name, Type, TypeId, constraint, normalise, zonk}; +use crate::core::{ForallBinder, KindOrType, Name, Type, TypeId, constraint, normalise, zonk}; use crate::state::{CheckState, UnificationEntry, UnificationState}; use crate::{ExternalQueries, safe_loop}; @@ -62,7 +62,10 @@ where } Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); - for &argument in synonym.arguments.iter() { + for application in synonym.arguments.iter() { + let argument = match application { + KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, + }; aux(graph, state, context, argument, dependent, visited_kinds)?; } } diff --git a/compiler-core/checking2/src/core/pretty.rs b/compiler-core/checking2/src/core/pretty.rs index 88e508b1..751dc11a 100644 --- a/compiler-core/checking2/src/core/pretty.rs +++ b/compiler-core/checking2/src/core/pretty.rs @@ -7,8 +7,8 @@ use rustc_hash::FxHashMap; use smol_str::{SmolStr, SmolStrBuilder}; use crate::core::{ - ForallBinder, ForallBinderId, Name, RowField, RowType, RowTypeId, SmolStrId, Synonym, - SynonymId, Type, TypeId, + ForallBinder, ForallBinderId, KindOrType, Name, RowField, RowType, RowTypeId, SmolStrId, + Synonym, SynonymId, Type, TypeId, }; use crate::{CheckedModule, ExternalQueries}; @@ -159,7 +159,12 @@ where let arguments = synonym .arguments .iter() - .map(|&argument| self.traverse(Precedence::Atom, argument)) + .map(|application| match application { + KindOrType::Kind(argument) => { + self.arena.text("@").append(self.traverse(Precedence::Atom, *argument)) + } + KindOrType::Type(argument) => self.traverse(Precedence::Atom, *argument), + }) .collect_vec(); let arguments = diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 6b848940..72ecc6f9 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -10,7 +10,7 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Depth, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise}; +use crate::core::{Depth, KindOrType, Name, RowField, RowType, RowTypeId, Type, TypeId, normalise}; use crate::error::ErrorKind; use crate::source::types; use crate::state::{CheckState, UnificationEntry}; @@ -459,10 +459,18 @@ where return Ok(CanUnify::Apart); } - iter::zip(&*t1_synonym.arguments, &*t2_synonym.arguments) - .try_fold(CanUnify::Equal, |result, (&a1, &a2)| { - result.and_then(|| can_unify(state, context, a1, a2)) - }) + iter::zip(&*t1_synonym.arguments, &*t2_synonym.arguments).try_fold( + CanUnify::Equal, + |result, (a1, a2)| { + result.and_then(|| match (a1, a2) { + (KindOrType::Kind(a1), KindOrType::Kind(a2)) + | (KindOrType::Type(a1), KindOrType::Type(a2)) => { + can_unify(state, context, *a1, *a2) + } + _ => Ok(CanUnify::Apart), + }) + }, + ) } _ => Ok(CanUnify::Apart), @@ -570,7 +578,10 @@ where Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); - for &argument in synonym.arguments.iter() { + for application in synonym.arguments.iter() { + let argument = match application { + KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, + }; let result = check(promote, state, context, argument)?; if !matches!(result, PromoteResult::Ok) { return Ok(result); diff --git a/compiler-core/checking2/src/core/walk.rs b/compiler-core/checking2/src/core/walk.rs index d52d1078..6c9cb20c 100644 --- a/compiler-core/checking2/src/core/walk.rs +++ b/compiler-core/checking2/src/core/walk.rs @@ -5,7 +5,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::normalise::normalise; -use crate::core::{ForallBinder, Type, TypeId}; +use crate::core::{ForallBinder, KindOrType, Type, TypeId}; use crate::state::CheckState; pub enum WalkAction { @@ -49,7 +49,10 @@ where } Type::SynonymApplication(synonym_id) => { let synonym = context.queries.lookup_synonym(synonym_id); - for &argument in synonym.arguments.iter() { + for application in synonym.arguments.iter() { + let argument = match application { + KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, + }; walk_type(state, context, argument, walker)?; } } diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 18ee930c..bafb6069 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -5,7 +5,7 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Name, Type, TypeId, normalise, toolkit}; +use crate::core::{KindOrType, Name, Type, TypeId, normalise, toolkit}; use crate::error::ErrorKind; use crate::state::CheckState; @@ -245,7 +245,10 @@ where Type::SynonymApplication(synonym_id) => { let synonym = context.lookup_synonym(synonym_id); let mut contains = false; - for &argument in synonym.arguments.iter() { + for application in synonym.arguments.iter() { + let argument = match application { + KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, + }; contains |= contains_rigid_name(state, context, argument, name)?; } contains diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 10baeda6..95a20d4b 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -356,7 +356,8 @@ impl IsOperator for lowering::TypeId { }; let operator_kind = toolkit::lookup_file_type(state, context, file_id, item_id)?; - let function_type = context.queries.intern_type(Type::Constructor(target_file_id, target_item_id)); + let function_type = + context.queries.intern_type(Type::Constructor(target_file_id, target_item_id)); let (function_type, function_kind) = apply_type_argument(state, context, (function_type, operator_kind), left, left_kind)?; let (elaborated_type, _) = diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index f3b3e2a3..caefa512 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -8,7 +8,7 @@ use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, Name, Role, Type, TypeId, normalise, toolkit}; +use crate::core::{ForallBinder, KindOrType, Name, Role, Type, TypeId, normalise, toolkit}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; @@ -227,12 +227,16 @@ where Type::SynonymApplication(synonym_id) => { let synonym = inference.context.lookup_synonym(synonym_id); - for &argument in synonym.arguments.iter() { + for application in synonym.arguments.iter() { + let argument = match application { + KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, + }; infer_roles(inference, argument, mode.child())?; } } - Type::Constructor(_, _) | Type::Integer(_) + Type::Constructor(_, _) + | Type::Integer(_) | Type::String(_, _) | Type::Unification(_) | Type::Free(_) diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index c8abfe60..e53d8537 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -10,7 +10,7 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{Saturation, Synonym, Type, TypeId, normalise, toolkit, unification}; +use crate::core::{KindOrType, Saturation, Synonym, Type, TypeId, normalise, toolkit, unification}; use crate::error::ErrorKind; use crate::source::types; use crate::state::CheckState; @@ -122,12 +122,12 @@ where ); state.defer_expansion = defer_expansion; - let (argument_types, (_, synonym_kind)) = chain_result?; + let (applications, (_, synonym_kind)) = chain_result?; let synonym = Synonym { saturation: Saturation::Full, reference: (file_id, type_id), - arguments: Arc::from(argument_types), + arguments: Arc::from(applications), }; let synonym_id = context.intern_synonym(synonym); @@ -153,13 +153,13 @@ where Q: ExternalQueries, { let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); - let (argument_types, (_, synonym_kind)) = + let (applications, (_, synonym_kind)) = infer_synonym_application_chain(state, context, (function_type, function_kind), arguments)?; let synonym = Synonym { saturation: Saturation::Partial, reference: (file_id, type_id), - arguments: Arc::from(argument_types), + arguments: Arc::from(applications), }; let synonym_id = context.intern_synonym(synonym); @@ -173,30 +173,33 @@ fn infer_synonym_application_chain( context: &CheckContext, function: (TypeId, TypeId), arguments: &[lowering::TypeId], -) -> QueryResult<(Vec, (TypeId, TypeId))> +) -> QueryResult<(Vec, (TypeId, TypeId))> where Q: ExternalQueries, { - let mut argument_types = vec![]; + let mut applications = vec![]; let mut current_function = function; for &argument_id in arguments { - let (argument_type, result_function) = - infer_synonym_application_kind(state, context, current_function, argument_id)?; - - argument_types.push(argument_type); - current_function = result_function; + current_function = infer_synonym_application_kind( + state, + context, + &mut applications, + current_function, + argument_id, + )?; } - Ok((argument_types, current_function)) + Ok((applications, current_function)) } fn infer_synonym_application_kind( state: &mut CheckState, context: &CheckContext, + applications: &mut Vec, (function_type, function_kind): (TypeId, TypeId), argument: lowering::TypeId, -) -> QueryResult<(TypeId, (TypeId, TypeId))> +) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { @@ -207,7 +210,8 @@ where let (argument_type, _) = types::check_kind(state, context, argument, argument_kind)?; let result_kind = normalise::normalise(state, context, result_kind)?; let result_type = context.intern_application(function_type, argument_type); - Ok((argument_type, (result_type, result_kind))) + applications.push(KindOrType::Type(argument_type)); + Ok((result_type, result_kind)) } Type::Unification(unification_id) => { @@ -221,7 +225,8 @@ where let result_kind = normalise::normalise(state, context, result_u)?; let result_type = context.intern_application(function_type, argument_type); - Ok((argument_type, (result_type, result_kind))) + applications.push(KindOrType::Type(argument_type)); + Ok((result_type, result_kind)) } Type::Forall(binder_id, inner_kind) => { @@ -233,7 +238,14 @@ where let function_kind = SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; - infer_synonym_application_kind(state, context, (function_type, function_kind), argument) + applications.push(KindOrType::Kind(kind_argument)); + infer_synonym_application_kind( + state, + context, + applications, + (function_type, function_kind), + argument, + ) } _ => { @@ -254,7 +266,8 @@ where }); } - Ok((argument_type, (t, k))) + applications.push(KindOrType::Type(argument_type)); + Ok((t, k)) } } } diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 2a08fbd3..9d606e49 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -7,7 +7,9 @@ use smol_str::SmolStr; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{ForallBinder, RowField, RowType, Type, TypeId, normalise, toolkit, unification}; +use crate::core::{ + ForallBinder, KindOrType, RowField, RowType, Type, TypeId, normalise, toolkit, unification, +}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::{operator, synonym}; use crate::state::CheckState; @@ -582,10 +584,22 @@ where let mut synonym_kind = toolkit::lookup_file_type(state, context, file_id, type_id)?; - for _ in arguments.iter() { + for application in arguments.iter() { synonym_kind = normalise::normalise(state, context, synonym_kind)?; - match context.lookup_type(synonym_kind) { - Type::Function(_, result_kind) => synonym_kind = result_kind, + match (application, context.lookup_type(synonym_kind)) { + (KindOrType::Kind(argument), Type::Forall(binder_id, inner_kind)) => { + let binder = context.lookup_forall_binder(binder_id); + synonym_kind = SubstituteName::one( + state, + context, + binder.name, + *argument, + inner_kind, + )?; + } + (KindOrType::Type(_), Type::Function(_, result_kind)) => { + synonym_kind = result_kind; + } _ => return Ok(unknown), } } diff --git a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs index 7f91ec98..748a1a3d 100644 --- a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs +++ b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.purs @@ -7,6 +7,5 @@ type NaturalTransformation f g = forall a. f a -> g a infixr 4 type NaturalTransformation as ~> -type Desugared = NaturalTransformation Array Maybe - -type Operator = Array ~> Maybe +type Test1 = NaturalTransformation Array Maybe +type Test2 = Array ~> Maybe diff --git a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap index 6fa1fb7d..eee5f157 100644 --- a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap @@ -17,7 +17,7 @@ Operator :: Type Synonyms type NaturalTransformation f g = forall (a :: (k :: Type)). (f :: (k :: Type) -> Type) (a :: (k :: Type)) -> (g :: (k :: Type) -> Type) (a :: (k :: Type)) -type Desugared = NaturalTransformation Array Maybe +type Desugared = NaturalTransformation @Type Array Maybe type Operator = NaturalTransformation @Type Array Maybe Roles diff --git a/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.purs b/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.purs new file mode 100644 index 00000000..b0d7f762 --- /dev/null +++ b/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data RowBox :: Row Type -> Type +data RowBox r + +type Apply :: forall k. (k -> Type) -> k -> Type +type Apply f a = f a + +type RowDesugared = Apply RowBox () diff --git a/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.snap b/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.snap new file mode 100644 index 00000000..f25edea7 --- /dev/null +++ b/tests-integration/fixtures/checking2/138_synonym_kind_application/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +RowBox :: Row Type -> Type +Apply :: forall (k :: Type). ((k :: Type) -> Type) -> (k :: Type) -> Type +RowDesugared :: Type + +Synonyms +type Apply f a = (f :: (k :: Type) -> Type) (a :: (k :: Type)) +type RowDesugared = Apply @(Row Type) RowBox () + +Roles +RowBox = [Phantom] diff --git a/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.purs b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.purs new file mode 100644 index 00000000..697b2b68 --- /dev/null +++ b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.purs @@ -0,0 +1,9 @@ +module Main where + +type Apply :: forall k. (k -> Type) -> k -> Type +type Apply f = f + +infixr 5 type Apply as $ + +type Test1 = Apply Array Int +type Test2 = Array $ Int diff --git a/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap new file mode 100644 index 00000000..1ee36b4b --- /dev/null +++ b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap @@ -0,0 +1,30 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Apply :: forall (k :: Type). ((k :: Type) -> Type) -> (k :: Type) -> Type +$ :: forall (k :: Type). ((k :: Type) -> Type) -> (k :: Type) -> Type +Test1 :: Type +Test2 :: Type + +Synonyms +type Apply f = (f :: (k :: Type) -> Type) +type Test1 = Apply @Type Array Int +type Test2 = Apply @Type Array Int + +Errors +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [ + CheckingKind( + AstId(18), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 1e3b570c..ea5626a4 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -303,3 +303,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_136_row_open_record_main() { run_test("136_row_open_record", "Main"); } #[rustfmt::skip] #[test] fn test_137_type_operator_synonym_expansion_main() { run_test("137_type_operator_synonym_expansion", "Main"); } + +#[rustfmt::skip] #[test] fn test_138_synonym_kind_application_main() { run_test("138_synonym_kind_application", "Main"); } + +#[rustfmt::skip] #[test] fn test_139_synonym_operator_alias_main() { run_test("139_synonym_operator_alias", "Main"); } From 9bbcf8bd9184a48a91124978434c7005dabd489b Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 02:46:12 +0800 Subject: [PATCH 340/386] Implement binding-count aware signature inspection --- .../checking2/src/source/signature.rs | 33 +++++++++++++++++++ .../checking2/src/source/type_items.rs | 21 ++++++------ .../036_synonym_partial_defer/Main.snap | 2 +- .../Main.snap | 8 ++--- .../139_synonym_operator_alias/Main.snap | 13 -------- 5 files changed, 48 insertions(+), 29 deletions(-) diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs index ec552beb..da3d67d2 100644 --- a/compiler-core/checking2/src/source/signature.rs +++ b/compiler-core/checking2/src/source/signature.rs @@ -46,3 +46,36 @@ where Ok(InspectSignature { binders, arguments, result }) } + +pub fn inspect_signature_bindings( + state: &mut CheckState, + context: &CheckContext, + signature: (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult +where + Q: ExternalQueries, +{ + let (signature_id, signature_kind) = signature; + + let toolkit::InspectQuantified { binders, quantified } = + toolkit::inspect_quantified(state, context, signature_kind)?; + + let toolkit::InspectFunction { arguments, result } = + toolkit::inspect_function(state, context, quantified)?; + + if bindings.len() > arguments.len() { + state.insert_error(ErrorKind::TypeSignatureVariableMismatch { + id: signature_id, + expected: arguments.len() as u32, + actual: bindings.len() as u32, + }); + } + + let mut remaining = arguments.into_iter(); + + let arguments = remaining.by_ref().take(bindings.len()).collect(); + let result = context.intern_function_chain_iter(remaining, result); + + Ok(InspectSignature { binders, arguments, result }) +} diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 368a048b..f9a20027 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -18,7 +18,7 @@ use crate::core::{ unification, zonk, }; use crate::error::{ErrorCrumb, ErrorKind}; -use crate::source::types; +use crate::source::{signature, types}; use crate::state::CheckState; struct PendingDataType { @@ -309,7 +309,7 @@ fn check_data_equation_check( where Q: ExternalQueries, { - let signature = super::signature::inspect_signature(state, context, signature, bindings)?; + let signature = signature::inspect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } @@ -359,7 +359,7 @@ where { match (signature, binding.kind) { (Some(signature_kind), Some(binding_kind)) => { - let (binding_kind, _) = super::types::infer_kind(state, context, binding_kind)?; + let (binding_kind, _) = types::infer_kind(state, context, binding_kind)?; let valid = unification::subtype(state, context, signature_kind, binding_kind)?; if valid { Ok(binding_kind) } else { Ok(context.unknown("invalid variable kind")) } } @@ -369,7 +369,7 @@ where } (None, Some(binding_kind)) => { let (binding_kind, _) = - super::types::check_kind(state, context, binding_kind, context.prim.t)?; + types::check_kind(state, context, binding_kind, context.prim.t)?; Ok(binding_kind) } (None, None) => { @@ -422,7 +422,7 @@ where for &argument in arguments.iter() { state.with_error_crumb(ErrorCrumb::ConstructorArgument(argument), |state| { let (checked_argument, _) = - super::types::check_kind(state, context, argument, context.prim.t)?; + types::check_kind(state, context, argument, context.prim.t)?; checked_arguments.push(checked_argument); Ok(()) })?; @@ -581,7 +581,7 @@ where }; let synonym = if let Some(synonym) = synonym { - let (synonym, _) = super::types::check_kind(state, context, synonym, result)?; + let (synonym, _) = types::check_kind(state, context, synonym, result)?; synonym } else { context.unknown("invalid synonym type") @@ -601,7 +601,7 @@ fn check_synonym_equation_check( where Q: ExternalQueries, { - let signature = super::signature::inspect_signature( + let signature = signature::inspect_signature_bindings( state, context, (signature_id, signature_kind), @@ -674,7 +674,7 @@ where let mut superclasses = vec![]; for &constraint in constraints.iter() { let (superclass, _) = - super::types::check_kind(state, context, constraint, context.prim.constraint)?; + types::check_kind(state, context, constraint, context.prim.constraint)?; superclasses.push(superclass); } @@ -698,7 +698,7 @@ fn check_class_equation_check( where Q: ExternalQueries, { - let signature = super::signature::inspect_signature(state, context, signature, bindings)?; + let signature = signature::inspect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } @@ -743,8 +743,7 @@ where let Some(signature_id) = signature else { continue }; - let (member_type, _) = - super::types::check_kind(state, context, *signature_id, context.prim.t)?; + let (member_type, _) = types::check_kind(state, context, *signature_id, context.prim.t)?; members.push((member_id, member_type)); } diff --git a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap index c63d1d4d..84213d80 100644 --- a/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap +++ b/tests-integration/fixtures/checking2/036_synonym_partial_defer/Main.snap @@ -17,7 +17,7 @@ Bad :: ?[partial synonym application] Synonyms type ReaderT r m a = (r :: Type) -> (m :: Type -> Type) (a :: Type) type Apply f a = (f :: (k1 :: Type) -> (k2 :: Type)) (a :: (k1 :: Type)) -type Good = Apply (Apply (ReaderT Int) Identity) String +type Good = Apply @Type @Type (Apply @(Type -> Type) @(Type -> Type) (ReaderT Int) Identity) String type Bad = ?[partial synonym application] Roles diff --git a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap index eee5f157..9d6603f7 100644 --- a/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap +++ b/tests-integration/fixtures/checking2/137_type_operator_synonym_expansion/Main.snap @@ -11,14 +11,14 @@ Types Maybe :: Type -> Type NaturalTransformation :: forall (k :: Type). ((k :: Type) -> Type) -> ((k :: Type) -> Type) -> Type ~> :: forall (k :: Type). ((k :: Type) -> Type) -> ((k :: Type) -> Type) -> Type -Desugared :: Type -Operator :: Type +Test1 :: Type +Test2 :: Type Synonyms type NaturalTransformation f g = forall (a :: (k :: Type)). (f :: (k :: Type) -> Type) (a :: (k :: Type)) -> (g :: (k :: Type) -> Type) (a :: (k :: Type)) -type Desugared = NaturalTransformation @Type Array Maybe -type Operator = NaturalTransformation @Type Array Maybe +type Test1 = NaturalTransformation @Type Array Maybe +type Test2 = NaturalTransformation @Type Array Maybe Roles Maybe = [Representational] diff --git a/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap index 1ee36b4b..6f2ce5fa 100644 --- a/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap +++ b/tests-integration/fixtures/checking2/139_synonym_operator_alias/Main.snap @@ -15,16 +15,3 @@ Synonyms type Apply f = (f :: (k :: Type) -> Type) type Test1 = Apply @Type Array Int type Test2 = Apply @Type Array Int - -Errors -CheckError { - kind: CannotUnify { - t1: Id(5), - t2: Id(6), - }, - crumbs: [ - CheckingKind( - AstId(18), - ), - ], -} From 4e2387181532fa2fc5bc930f40e5048b49ceb165 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 17:20:43 +0800 Subject: [PATCH 341/386] Make variable naming consistent in matching functions --- .../core/constraint/compiler/prim_coerce.rs | 93 +++++++++++-------- .../checking2/src/core/constraint/given.rs | 70 +++++++------- .../src/core/constraint/instances.rs | 80 ++++++++++------ 3 files changed, 144 insertions(+), 99 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index 4b3a0c43..dc197def 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -1,3 +1,4 @@ +use std::iter; use std::sync::Arc; use building_types::QueryResult; @@ -387,50 +388,64 @@ where fn try_refl( state: &mut CheckState, context: &CheckContext, - t1: TypeId, - t2: TypeId, + t1_type: TypeId, + t2_type: TypeId, ) -> QueryResult where Q: ExternalQueries, { - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; + let t1_type = normalise::normalise(state, context, t1_type)?; + let t2_type = normalise::normalise(state, context, t2_type)?; - if t1 == t2 { + if t1_type == t2_type { return Ok(true); } - match (context.lookup_type(t1), context.lookup_type(t2)) { - (Type::Application(f1, a1), Type::Application(f2, a2)) - | (Type::Constrained(f1, a1), Type::Constrained(f2, a2)) - | (Type::KindApplication(f1, a1), Type::KindApplication(f2, a2)) - | (Type::Kinded(f1, a1), Type::Kinded(f2, a2)) => { - Ok(try_refl(state, context, f1, f2)? && try_refl(state, context, a1, a2)?) + match (context.lookup_type(t1_type), context.lookup_type(t2_type)) { + ( + Type::Application(t1_function, t1_argument), + Type::Application(t2_function, t2_argument), + ) + | ( + Type::Constrained(t1_function, t1_argument), + Type::Constrained(t2_function, t2_argument), + ) + | ( + Type::KindApplication(t1_function, t1_argument), + Type::KindApplication(t2_function, t2_argument), + ) + | (Type::Kinded(t1_function, t1_argument), Type::Kinded(t2_function, t2_argument)) => { + Ok(try_refl(state, context, t1_function, t2_function)? + && try_refl(state, context, t1_argument, t2_argument)?) } - (Type::Function(a1, r1), Type::Function(a2, r2)) => { - Ok(try_refl(state, context, a1, a2)? && try_refl(state, context, r1, r2)?) + (Type::Function(t1_argument, t1_result), Type::Function(t2_argument, t2_result)) => { + Ok(try_refl(state, context, t1_argument, t2_argument)? + && try_refl(state, context, t1_result, t2_result)?) } - (Type::Forall(b1, i1), Type::Forall(b2, i2)) => { - let b1 = context.lookup_forall_binder(b1); - let b2 = context.lookup_forall_binder(b2); - Ok(try_refl(state, context, b1.kind, b2.kind)? && try_refl(state, context, i1, i2)?) + (Type::Forall(t1_binder_id, t1_inner), Type::Forall(t2_binder_id, t2_inner)) => { + let t1_binder = context.lookup_forall_binder(t1_binder_id); + let t2_binder = context.lookup_forall_binder(t2_binder_id); + Ok(try_refl(state, context, t1_binder.kind, t2_binder.kind)? + && try_refl(state, context, t1_inner, t2_inner)?) } - (Type::SynonymApplication(s1), Type::SynonymApplication(s2)) => { - let s1 = context.lookup_synonym(s1); - let s2 = context.lookup_synonym(s2); - if s1.reference != s2.reference || s1.arguments.len() != s2.arguments.len() { + (Type::SynonymApplication(t1_synonym_id), Type::SynonymApplication(t2_synonym_id)) => { + let t1_synonym = context.lookup_synonym(t1_synonym_id); + let t2_synonym = context.lookup_synonym(t2_synonym_id); + if t1_synonym.reference != t2_synonym.reference + || t1_synonym.arguments.len() != t2_synonym.arguments.len() + { return Ok(false); } - let s1_args = Arc::clone(&s1.arguments); - let s2_args = Arc::clone(&s2.arguments); - for (a1, a2) in s1_args.iter().zip(s2_args.iter()) { - let equivalent = match (a1, a2) { - (KindOrType::Kind(a1), KindOrType::Kind(a2)) - | (KindOrType::Type(a1), KindOrType::Type(a2)) => { - try_refl(state, context, *a1, *a2)? + let t1_arguments = Arc::clone(&t1_synonym.arguments); + let t2_arguments = Arc::clone(&t2_synonym.arguments); + for (t1_argument, t2_argument) in iter::zip(t1_arguments.iter(), t2_arguments.iter()) { + let equivalent = match (t1_argument, t2_argument) { + (KindOrType::Kind(t1_kind), KindOrType::Kind(t2_kind)) + | (KindOrType::Type(t1_kind), KindOrType::Type(t2_kind)) => { + try_refl(state, context, *t1_kind, *t2_kind)? } _ => false, }; @@ -441,26 +456,28 @@ where Ok(true) } - (Type::Row(r1), Type::Row(r2)) => { - let r1 = context.lookup_row_type(r1); - let r2 = context.lookup_row_type(r2); - if r1.fields.len() != r2.fields.len() { + (Type::Row(t1_row_id), Type::Row(t2_row_id)) => { + let t1_row = context.lookup_row_type(t1_row_id); + let t2_row = context.lookup_row_type(t2_row_id); + if t1_row.fields.len() != t2_row.fields.len() { return Ok(false); } - for (f1, f2) in r1.fields.iter().zip(r2.fields.iter()) { - if f1.label != f2.label || !try_refl(state, context, f1.id, f2.id)? { + for (t1_field, t2_field) in iter::zip(t1_row.fields.iter(), t2_row.fields.iter()) { + if t1_field.label != t2_field.label + || !try_refl(state, context, t1_field.id, t2_field.id)? + { return Ok(false); } } - match (r1.tail, r2.tail) { - (Some(t1), Some(t2)) => try_refl(state, context, t1, t2), + match (t1_row.tail, t2_row.tail) { + (Some(t1_tail), Some(t2_tail)) => try_refl(state, context, t1_tail, t2_tail), (None, None) => Ok(true), _ => Ok(false), } } - (Type::Rigid(n1, _, k1), Type::Rigid(n2, _, k2)) => { - Ok(n1 == n2 && try_refl(state, context, k1, k2)?) + (Type::Rigid(t1_name, _, t1_kind), Type::Rigid(t2_name, _, t2_kind)) => { + Ok(t1_name == t2_name && try_refl(state, context, t1_kind, t2_kind)?) } _ => Ok(false), diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index 2d4b16b0..30d17e54 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -1,3 +1,5 @@ +use std::iter; + use building_types::QueryResult; use itertools::Itertools; @@ -74,7 +76,7 @@ where let mut stuck_positions = vec![]; for (index, (&wanted_argument, &given_argument)) in - wanted.arguments.iter().zip(given.arguments.iter()).enumerate() + iter::zip(wanted.arguments.iter(), given.arguments.iter()).enumerate() { let match_result = match_given_type(state, context, wanted_argument, given_argument)?; @@ -132,31 +134,33 @@ where match (wanted_t, given_t) { (Type::Unification(_), _) => Ok(MatchType::Stuck), - (Type::Rigid(wname, _, wkind), Type::Rigid(gname, _, gkind)) => { - if wname == gname { - match_given_type(state, context, wkind, gkind) + (Type::Rigid(wanted_name, _, wanted_kind), Type::Rigid(given_name, _, given_kind)) => { + if wanted_name == given_name { + match_given_type(state, context, wanted_kind, given_kind) } else { Ok(MatchType::Apart) } } - (Type::Application(wf, wa), Type::Application(gf, ga)) => { - match_given_type(state, context, wf, gf)? - .and_then(|| match_given_type(state, context, wa, ga)) - } + ( + Type::Application(wanted_function, wanted_argument), + Type::Application(given_function, given_argument), + ) => match_given_type(state, context, wanted_function, given_function)? + .and_then(|| match_given_type(state, context, wanted_argument, given_argument)), - (Type::Function(wa, wr), Type::Function(ga, gr)) => { - match_given_type(state, context, wa, ga)? - .and_then(|| match_given_type(state, context, wr, gr)) - } + ( + Type::Function(wanted_argument, wanted_result), + Type::Function(given_argument, given_result), + ) => match_given_type(state, context, wanted_argument, given_argument)? + .and_then(|| match_given_type(state, context, wanted_result, given_result)), - (Type::Function(wa, wr), Type::Application(_, _)) => { - let wanted = context.intern_function_application(wa, wr); + (Type::Function(wanted_argument, wanted_result), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wanted_argument, wanted_result); match_given_type(state, context, wanted, given) } - (Type::Application(_, _), Type::Function(ga, gr)) => { - let given = context.intern_function_application(ga, gr); + (Type::Application(_, _), Type::Function(given_argument, given_result)) => { + let given = context.intern_function_application(given_argument, given_result); match_given_type(state, context, wanted, given) } @@ -169,7 +173,8 @@ where } let mut result = MatchType::Match; - for (wanted_field, given_field) in wanted_row.fields.iter().zip(given_row.fields.iter()) + for (wanted_field, given_field) in + iter::zip(wanted_row.fields.iter(), given_row.fields.iter()) { if wanted_field.label != given_field.label { return Ok(MatchType::Apart); @@ -203,26 +208,29 @@ where } } - (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { - match_given_type(state, context, wf, gf)? - .and_then(|| match_given_type(state, context, wa, ga)) - } + ( + Type::KindApplication(wanted_function, wanted_argument), + Type::KindApplication(given_function, given_argument), + ) => match_given_type(state, context, wanted_function, given_function)? + .and_then(|| match_given_type(state, context, wanted_argument, given_argument)), - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { - let wsyn = context.lookup_synonym(wsyn); - let gsyn = context.lookup_synonym(gsyn); + (Type::SynonymApplication(wanted_synonym), Type::SynonymApplication(given_synonym)) => { + let wanted_synonym = context.lookup_synonym(wanted_synonym); + let given_synonym = context.lookup_synonym(given_synonym); - if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + if wanted_synonym.reference != given_synonym.reference + || wanted_synonym.arguments.len() != given_synonym.arguments.len() + { return Ok(MatchType::Apart); } - wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( + iter::zip(wanted_synonym.arguments.iter(), given_synonym.arguments.iter()).try_fold( MatchType::Match, - |result, (wa, ga)| { - result.and_then(|| match (wa, ga) { - (KindOrType::Kind(wa), KindOrType::Kind(ga)) - | (KindOrType::Type(wa), KindOrType::Type(ga)) => { - match_given_type(state, context, *wa, *ga) + |result, (wanted_argument, given_argument)| { + result.and_then(|| match (wanted_argument, given_argument) { + (KindOrType::Kind(wanted_kind), KindOrType::Kind(given_kind)) + | (KindOrType::Type(wanted_kind), KindOrType::Type(given_kind)) => { + match_given_type(state, context, *wanted_kind, *given_kind) } _ => Ok(MatchType::Apart), }) diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index 71e1ebf3..cf5a3a76 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::iter; use building_types::QueryResult; use files::FileId; @@ -199,7 +200,7 @@ where let mut stuck_positions = vec![]; for (index, (&wanted_argument, &instance_argument)) in - wanted.arguments.iter().zip(decomposed.arguments.iter()).enumerate() + iter::zip(wanted.arguments.iter(), decomposed.arguments.iter()).enumerate() { let match_result = match_type( state, @@ -315,51 +316,70 @@ where match_row_type(state, context, bindings, equalities, wanted_row, given_row) } - (Type::Application(wf, wa), Type::Application(gf, ga)) => { - match_type(state, context, bindings, equalities, wf, gf)? - .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) - } + ( + Type::Application(wanted_function, wanted_argument), + Type::Application(given_function, given_argument), + ) => match_type(state, context, bindings, equalities, wanted_function, given_function)? + .and_then(|| { + match_type(state, context, bindings, equalities, wanted_argument, given_argument) + }), - (Type::Function(wa, wr), Type::Function(ga, gr)) => { - match_type(state, context, bindings, equalities, wa, ga)? - .and_then(|| match_type(state, context, bindings, equalities, wr, gr)) - } + ( + Type::Function(wanted_argument, wanted_result), + Type::Function(given_argument, given_result), + ) => match_type(state, context, bindings, equalities, wanted_argument, given_argument)? + .and_then(|| { + match_type(state, context, bindings, equalities, wanted_result, given_result) + }), - (Type::Function(wa, wr), Type::Application(_, _)) => { - let wanted = context.intern_function_application(wa, wr); + (Type::Function(wanted_argument, wanted_result), Type::Application(_, _)) => { + let wanted = context.intern_function_application(wanted_argument, wanted_result); match_type(state, context, bindings, equalities, wanted, given) } - (Type::Application(_, _), Type::Function(ga, gr)) => { - let given = context.intern_function_application(ga, gr); + (Type::Application(_, _), Type::Function(given_argument, given_result)) => { + let given = context.intern_function_application(given_argument, given_result); match_type(state, context, bindings, equalities, wanted, given) } - (Type::KindApplication(wf, wa), Type::KindApplication(gf, ga)) => { - match_type(state, context, bindings, equalities, wf, gf)? - .and_then(|| match_type(state, context, bindings, equalities, wa, ga)) - } + ( + Type::KindApplication(wanted_function, wanted_argument), + Type::KindApplication(given_function, given_argument), + ) => match_type(state, context, bindings, equalities, wanted_function, given_function)? + .and_then(|| { + match_type(state, context, bindings, equalities, wanted_argument, given_argument) + }), - (Type::Kinded(wi, wk), Type::Kinded(gi, gk)) => { - match_type(state, context, bindings, equalities, wi, gi)? - .and_then(|| match_type(state, context, bindings, equalities, wk, gk)) + (Type::Kinded(wanted_inner, wanted_kind), Type::Kinded(given_inner, given_kind)) => { + match_type(state, context, bindings, equalities, wanted_inner, given_inner)?.and_then( + || match_type(state, context, bindings, equalities, wanted_kind, given_kind), + ) } - (Type::SynonymApplication(wsyn), Type::SynonymApplication(gsyn)) => { - let wsyn = context.lookup_synonym(wsyn); - let gsyn = context.lookup_synonym(gsyn); + (Type::SynonymApplication(wanted_synonym), Type::SynonymApplication(given_synonym)) => { + let wanted_synonym = context.lookup_synonym(wanted_synonym); + let given_synonym = context.lookup_synonym(given_synonym); - if wsyn.reference != gsyn.reference || wsyn.arguments.len() != gsyn.arguments.len() { + if wanted_synonym.reference != given_synonym.reference + || wanted_synonym.arguments.len() != given_synonym.arguments.len() + { return Ok(MatchType::Apart); } - wsyn.arguments.iter().zip(gsyn.arguments.iter()).try_fold( + iter::zip(wanted_synonym.arguments.iter(), given_synonym.arguments.iter()).try_fold( MatchType::Match, - |result, (wa, ga)| { - result.and_then(|| match (wa, ga) { - (KindOrType::Kind(wa), KindOrType::Kind(ga)) - | (KindOrType::Type(wa), KindOrType::Type(ga)) => { - match_type(state, context, bindings, equalities, *wa, *ga) + |result, (wanted_argument, given_argument)| { + result.and_then(|| match (wanted_argument, given_argument) { + (KindOrType::Kind(wanted_kind), KindOrType::Kind(given_kind)) + | (KindOrType::Type(wanted_kind), KindOrType::Type(given_kind)) => { + match_type( + state, + context, + bindings, + equalities, + *wanted_kind, + *given_kind, + ) } _ => Ok(MatchType::Apart), }) From 2d5ead103848fd4649d488989ba6c2e62f3fd803 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 18:43:53 +0800 Subject: [PATCH 342/386] Add test for different forms of const --- .../140_const_equation_forms/Main.purs | 10 ++ .../140_const_equation_forms/Main.snap | 11 +++ .../141_const_forms_synonym_arrow/Main.purs | 21 ++++ .../141_const_forms_synonym_arrow/Main.snap | 76 +++++++++++++++ .../142_const_forms_synonym_forall/Main.purs | 21 ++++ .../142_const_forms_synonym_forall/Main.snap | 76 +++++++++++++++ .../143_const_forms_fn_alias/Main.purs | 22 +++++ .../143_const_forms_fn_alias/Main.snap | 95 +++++++++++++++++++ .../tests/checking2/generated.rs | 8 ++ 9 files changed, 340 insertions(+) create mode 100644 tests-integration/fixtures/checking2/140_const_equation_forms/Main.purs create mode 100644 tests-integration/fixtures/checking2/140_const_equation_forms/Main.snap create mode 100644 tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs create mode 100644 tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap create mode 100644 tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs create mode 100644 tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap create mode 100644 tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.purs create mode 100644 tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap diff --git a/tests-integration/fixtures/checking2/140_const_equation_forms/Main.purs b/tests-integration/fixtures/checking2/140_const_equation_forms/Main.purs new file mode 100644 index 00000000..b6571cee --- /dev/null +++ b/tests-integration/fixtures/checking2/140_const_equation_forms/Main.purs @@ -0,0 +1,10 @@ +module Main where + +const :: forall a b. a -> b -> a +const a b = a + +const2 :: forall a b. a -> b -> a +const2 a = \b -> a + +const3 :: forall a b. a -> b -> a +const3 = \a b -> a diff --git a/tests-integration/fixtures/checking2/140_const_equation_forms/Main.snap b/tests-integration/fixtures/checking2/140_const_equation_forms/Main.snap new file mode 100644 index 00000000..c5921108 --- /dev/null +++ b/tests-integration/fixtures/checking2/140_const_equation_forms/Main.snap @@ -0,0 +1,11 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +const :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +const2 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +const3 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs new file mode 100644 index 00000000..d6e30124 --- /dev/null +++ b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs @@ -0,0 +1,21 @@ +module Main where + +type Const a b = a -> b -> a + +const0 :: forall a b. Const a b +const0 = \a b -> a + +const1 :: forall a b. Const a b +const1 a b = a + +const2 :: forall a b. Const a b +const2 a = \b -> a + +const3 :: forall a b. Const a b +const3 = \a -> \b -> a + +const4 :: forall a b. a -> b -> a +const4 a b = const1 a b + +const5 :: forall a b. a -> b -> a +const5 a = const2 a diff --git a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap new file mode 100644 index 00000000..398de9ea --- /dev/null +++ b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap @@ -0,0 +1,76 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +const0 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) +const1 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) +const2 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) +const3 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) +const4 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +const5 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) + +Types +Const :: Type -> Type -> Type + +Synonyms +type Const a b = (a :: Type) -> (b :: Type) -> (a :: Type) + +Errors +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(30), + ), + expected: 0, + actual: 2, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(45), + ), + expected: 0, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + CheckingExpression( + AstId(94), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + CheckingExpression( + AstId(114), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs new file mode 100644 index 00000000..59ccf3ad --- /dev/null +++ b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs @@ -0,0 +1,21 @@ +module Main where + +type ConstPoly = forall a b. a -> b -> a + +poly0 :: ConstPoly +poly0 = \a b -> a + +poly1 :: ConstPoly +poly1 a b = a + +poly2 :: ConstPoly +poly2 a = \b -> a + +poly3 :: ConstPoly +poly3 = \a -> \b -> a + +poly4 :: forall a b. a -> b -> a +poly4 a b = poly1 a b + +poly5 :: forall a b. a -> b -> a +poly5 a = poly2 a diff --git a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap new file mode 100644 index 00000000..8062b430 --- /dev/null +++ b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap @@ -0,0 +1,76 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +poly0 :: ConstPoly +poly1 :: ConstPoly +poly2 :: ConstPoly +poly3 :: ConstPoly +poly4 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +poly5 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) + +Types +ConstPoly :: Type + +Synonyms +type ConstPoly = forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) + +Errors +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(25), + ), + expected: 0, + actual: 2, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(34), + ), + expected: 0, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + CheckingExpression( + AstId(71), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + CheckingExpression( + AstId(91), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.purs b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.purs new file mode 100644 index 00000000..6d69f3ec --- /dev/null +++ b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.purs @@ -0,0 +1,22 @@ +module Main where + +type Fn b a = b -> a +type ConstFn a b = a -> Fn b a + +fn0 :: forall a b. ConstFn a b +fn0 = \a b -> a + +fn1 :: forall a b. ConstFn a b +fn1 a b = a + +fn2 :: forall a b. ConstFn a b +fn2 a = \b -> a + +fn3 :: forall a b. ConstFn a b +fn3 = \a -> \b -> a + +fn4 :: forall b. forall a. a -> Fn b a +fn4 a b = a + +fn5 :: forall b. forall a. a -> Fn b a +fn5 a = \b -> a diff --git a/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap new file mode 100644 index 00000000..cabbbdb0 --- /dev/null +++ b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap @@ -0,0 +1,95 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +fn0 :: forall (a :: Type) (b :: Type). ConstFn (a :: Type) (b :: Type) +fn1 :: forall (a :: Type) (b :: Type). ConstFn (a :: Type) (b :: Type) +fn2 :: forall (a :: Type) (b :: Type). ConstFn (a :: Type) (b :: Type) +fn3 :: forall (a :: Type) (b :: Type). ConstFn (a :: Type) (b :: Type) +fn4 :: forall (b :: Type) (a :: Type). (a :: Type) -> Fn (b :: Type) (a :: Type) +fn5 :: forall (b :: Type) (a :: Type). (a :: Type) -> Fn (b :: Type) (a :: Type) + +Types +Fn :: Type -> Type -> Type +ConstFn :: Type -> Type -> Type + +Synonyms +type Fn b a = (b :: Type) -> (a :: Type) +type ConstFn a b = (a :: Type) -> Fn (b :: Type) (a :: Type) + +Errors +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(37), + ), + expected: 0, + actual: 2, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(52), + ), + expected: 0, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + ], +} +CheckError { + kind: TooManyBinders { + signature: Some( + AstId(87), + ), + expected: 1, + actual: 2, + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + CheckingExpression( + AstId(103), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + CheckingExpression( + AstId(120), + ), + CheckingExpression( + AstId(123), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index ea5626a4..f3520cca 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -307,3 +307,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_138_synonym_kind_application_main() { run_test("138_synonym_kind_application", "Main"); } #[rustfmt::skip] #[test] fn test_139_synonym_operator_alias_main() { run_test("139_synonym_operator_alias", "Main"); } + +#[rustfmt::skip] #[test] fn test_140_const_equation_forms_main() { run_test("140_const_equation_forms", "Main"); } + +#[rustfmt::skip] #[test] fn test_141_const_forms_synonym_arrow_main() { run_test("141_const_forms_synonym_arrow", "Main"); } + +#[rustfmt::skip] #[test] fn test_142_const_forms_synonym_forall_main() { run_test("142_const_forms_synonym_forall", "Main"); } + +#[rustfmt::skip] #[test] fn test_143_const_forms_fn_alias_main() { run_test("143_const_forms_fn_alias", "Main"); } From ec4a528356ba6f4ef10cdf2eedea62bd1277473f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 21:04:04 +0800 Subject: [PATCH 343/386] Improve handling for synonyms in signatures --- compiler-core/checking2/src/core/normalise.rs | 115 +++++++++++++++++- compiler-core/checking2/src/core/toolkit.rs | 59 ++++++--- .../checking2/src/source/signature.rs | 4 +- .../checking2/src/source/terms/equations.rs | 10 +- .../checking2/src/source/terms/form_let.rs | 11 +- .../141_const_forms_synonym_arrow/Main.purs | 6 - .../141_const_forms_synonym_arrow/Main.snap | 60 --------- .../142_const_forms_synonym_forall/Main.purs | 6 - .../142_const_forms_synonym_forall/Main.snap | 60 --------- .../143_const_forms_fn_alias/Main.snap | 75 ------------ .../Main.purs | 6 + .../Main.snap | 17 +++ .../Main.purs | 7 ++ .../Main.snap | 18 +++ .../Main.purs | 6 + .../Main.snap | 14 +++ .../Main.purs | 14 +++ .../Main.snap | 20 +++ .../tests/checking2/generated.rs | 8 ++ 19 files changed, 284 insertions(+), 232 deletions(-) create mode 100644 tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.purs create mode 100644 tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.snap create mode 100644 tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.purs create mode 100644 tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.snap create mode 100644 tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.purs create mode 100644 tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.snap create mode 100644 tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.purs create mode 100644 tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.snap diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index a79c5364..f1bd42b3 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -2,9 +2,11 @@ use building_types::QueryResult; use itertools::Itertools; +use rustc_hash::FxHashMap; use crate::context::CheckContext; -use crate::core::{RowType, Type, TypeId}; +use crate::core::substitute::{NameToType, SubstituteName}; +use crate::core::{KindOrType, RowType, Saturation, Type, TypeId, toolkit}; use crate::state::{CheckState, UnificationState}; use crate::{ExternalQueries, safe_loop}; @@ -115,3 +117,114 @@ where Ok(id) } + +/// Expands [`Type::SynonymApplication`] with respect to oversaturation. +/// +/// In certain cases, type synonyms can be oversaturated or applied with more +/// arguments than they're declared to accept. In the following example: +/// +/// ```purescript +/// type Identity :: forall k. k -> k +/// type Identity a = a +/// +/// data Tuple a b = Tuple a b +/// +/// test1 :: Identity Array Int +/// test1 = [42] +/// +/// test2 :: Identity Tuple Int String +/// test2 = Tuple 42 "hello" +/// +/// forceSolve = { test1, test2 } +/// ``` +/// +/// The `Identity Array` and `Identity Tuple` will be expanded to reveal +/// `Array` and `Tuple` which are applied to their respective arguments. +fn expand_synonym( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut current = id; + let mut arguments = vec![]; + + safe_loop! { + current = normalise(state, context, current)?; + match context.lookup_type(current) { + Type::Application(function, argument) => { + arguments.push(KindOrType::Type(argument)); + current = function; + } + Type::KindApplication(function, argument) => { + arguments.push(KindOrType::Kind(argument)); + current = function; + } + _ => break, + } + } + + current = normalise(state, context, current)?; + let Type::SynonymApplication(synonym_id) = context.lookup_type(current) else { + return Ok(id); + }; + + let synonym = context.lookup_synonym(synonym_id); + if synonym.saturation != Saturation::Full { + return Ok(id); + } + + let (file_id, type_id) = synonym.reference; + let checked_synonym = toolkit::lookup_file_synonym(state, context, file_id, type_id)?; + let Some(checked_synonym) = checked_synonym else { + return Ok(id); + }; + + let mut parameter_arguments = synonym.arguments.iter().filter_map(|argument| match argument { + KindOrType::Type(argument) => Some(*argument), + KindOrType::Kind(_) => None, + }); + + let mut bindings: NameToType = FxHashMap::default(); + for parameter in &checked_synonym.parameters { + let Some(argument) = parameter_arguments.next() else { + return Ok(id); + }; + bindings.insert(parameter.name, argument); + } + + let mut expanded = if bindings.is_empty() { + checked_synonym.synonym + } else { + SubstituteName::many(state, context, &bindings, checked_synonym.synonym)? + }; + + for argument in arguments.into_iter().rev() { + expanded = match argument { + KindOrType::Type(argument) => context.intern_application(expanded, argument), + KindOrType::Kind(argument) => context.intern_kind_application(expanded, argument), + }; + } + + Ok(expanded) +} + +pub fn normalise_expand( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + let expanded = expand_synonym(state, context, id)?; + let normalised = normalise(state, context, expanded)?; + if normalised == id { + return Ok(id); + } + id = normalised; + } +} diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 15d53cb2..44e1a36c 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -26,6 +26,12 @@ pub struct InspectFunction { pub result: TypeId, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InspectMode { + Some(usize), + Full, +} + pub struct DecomposedInstance { pub binders: Vec, pub constraints: Vec, @@ -243,7 +249,7 @@ where let mut current = id; safe_loop! { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; @@ -261,6 +267,18 @@ pub fn inspect_function( context: &CheckContext, id: TypeId, ) -> QueryResult +where + Q: ExternalQueries, +{ + inspect_function_with(state, context, id, InspectMode::Full) +} + +pub fn inspect_function_with( + state: &mut CheckState, + context: &CheckContext, + id: TypeId, + mode: InspectMode, +) -> QueryResult where Q: ExternalQueries, { @@ -268,33 +286,38 @@ where let mut current = id; safe_loop! { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; + + if let InspectMode::Some(required) = mode + && arguments.len() >= required + { + return Ok(InspectFunction { arguments, result: current }); + } + match context.lookup_type(current) { Type::Function(argument, result) => { arguments.push(argument); current = result; } Type::Application(function_argument, result) => { - let function_argument = normalise::normalise(state, context, function_argument)?; + let function_argument = normalise::normalise_expand(state, context, function_argument)?; let Type::Application(function, argument) = context.lookup_type(function_argument) else { - break; + return Ok(InspectFunction { arguments, result: current }); }; - let function = normalise::normalise(state, context, function)?; + let function = normalise::normalise_expand(state, context, function)?; if function == context.prim.function { arguments.push(argument); current = result; } else { - break; + return Ok(InspectFunction { arguments, result: current }); } } - _ => break, + _ => return Ok(InspectFunction { arguments, result: current }), } } - - Ok(InspectFunction { arguments, result: current }) } pub fn decompose_instance( @@ -312,7 +335,7 @@ where let mut constraints = vec![]; safe_loop! { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; match context.lookup_type(current) { Type::Constrained(constraint, constrained) => { constraints.push(constraint); @@ -344,7 +367,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; let Type::Forall(binder_id, inner) = context.lookup_type(id) else { break; @@ -370,7 +393,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; let Type::Forall(binder_id, inner) = context.lookup_type(id) else { break; @@ -396,7 +419,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_wanted(constraint); @@ -417,7 +440,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_given(constraint); @@ -438,7 +461,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(_, constrained) => { id = constrained; @@ -469,7 +492,7 @@ pub fn decompose_function( where Q: ExternalQueries, { - let t = normalise::normalise(state, context, t)?; + let t = normalise::normalise_expand(state, context, t)?; match context.lookup_type(t) { Type::Function(argument, result) => Ok(Some((argument, result))), @@ -485,9 +508,9 @@ where } Type::Application(partial, result) => { - let partial = normalise::normalise(state, context, partial)?; + let partial = normalise::normalise_expand(state, context, partial)?; if let Type::Application(constructor, argument) = context.lookup_type(partial) { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.function { return Ok(Some((argument, result))); } diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs index da3d67d2..67c0cff6 100644 --- a/compiler-core/checking2/src/source/signature.rs +++ b/compiler-core/checking2/src/source/signature.rs @@ -31,7 +31,7 @@ where toolkit::inspect_quantified(state, context, signature_kind)?; let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function(state, context, quantified)?; + toolkit::inspect_function_with(state, context, quantified, toolkit::InspectMode::Full)?; if bindings.len() > arguments.len() { state.insert_error(ErrorKind::TypeSignatureVariableMismatch { @@ -62,7 +62,7 @@ where toolkit::inspect_quantified(state, context, signature_kind)?; let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function(state, context, quantified)?; + toolkit::inspect_function_with(state, context, quantified, toolkit::InspectMode::Full)?; if bindings.len() > arguments.len() { state.insert_error(ErrorKind::TypeSignatureVariableMismatch { diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index 6801f1f7..e92293af 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -25,13 +25,19 @@ pub fn check_equations_with( where Q: ExternalQueries, { + let required = equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); + let toolkit::InspectQuantified { quantified, .. } = toolkit::inspect_quantified(state, context, expected_type)?; let quantified = toolkit::collect_givens(state, context, quantified)?; - let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function(state, context, quantified)?; + let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function_with( + state, + context, + quantified, + toolkit::InspectMode::Some(required), + )?; let function = context.intern_function_chain(&arguments, result); check_equations_core(state, context, origin, &arguments, result, function, equations)?; diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 18a9b149..39a0f839 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -139,13 +139,20 @@ where }; if let Some(signature_id) = name.signature { + let required = + name.equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); + let toolkit::InspectQuantified { quantified, .. } = toolkit::inspect_quantified(state, context, name_type)?; let quantified = toolkit::collect_givens(state, context, quantified)?; - let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function(state, context, quantified)?; + let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function_with( + state, + context, + quantified, + toolkit::InspectMode::Some(required), + )?; let function = context.intern_function_chain(&arguments, result); diff --git a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs index d6e30124..238c3f3a 100644 --- a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs +++ b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.purs @@ -13,9 +13,3 @@ const2 a = \b -> a const3 :: forall a b. Const a b const3 = \a -> \b -> a - -const4 :: forall a b. a -> b -> a -const4 a b = const1 a b - -const5 :: forall a b. a -> b -> a -const5 a = const2 a diff --git a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap index 398de9ea..65cb0fef 100644 --- a/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap +++ b/tests-integration/fixtures/checking2/141_const_forms_synonym_arrow/Main.snap @@ -8,69 +8,9 @@ const0 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) const1 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) const2 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) const3 :: forall (a :: Type) (b :: Type). Const (a :: Type) (b :: Type) -const4 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) -const5 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) Types Const :: Type -> Type -> Type Synonyms type Const a b = (a :: Type) -> (b :: Type) -> (a :: Type) - -Errors -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(30), - ), - expected: 0, - actual: 2, - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(45), - ), - expected: 0, - actual: 1, - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(9), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - CheckingExpression( - AstId(94), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(10), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - CheckingExpression( - AstId(114), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs index 59ccf3ad..4fb553d6 100644 --- a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs +++ b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.purs @@ -13,9 +13,3 @@ poly2 a = \b -> a poly3 :: ConstPoly poly3 = \a -> \b -> a - -poly4 :: forall a b. a -> b -> a -poly4 a b = poly1 a b - -poly5 :: forall a b. a -> b -> a -poly5 a = poly2 a diff --git a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap index 8062b430..c62c8559 100644 --- a/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap +++ b/tests-integration/fixtures/checking2/142_const_forms_synonym_forall/Main.snap @@ -8,69 +8,9 @@ poly0 :: ConstPoly poly1 :: ConstPoly poly2 :: ConstPoly poly3 :: ConstPoly -poly4 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) -poly5 :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) Types ConstPoly :: Type Synonyms type ConstPoly = forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) - -Errors -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(25), - ), - expected: 0, - actual: 2, - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(34), - ), - expected: 0, - actual: 1, - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(9), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - CheckingExpression( - AstId(71), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(10), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - CheckingExpression( - AstId(91), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap index cabbbdb0..2b87cbfe 100644 --- a/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap +++ b/tests-integration/fixtures/checking2/143_const_forms_fn_alias/Main.snap @@ -18,78 +18,3 @@ ConstFn :: Type -> Type -> Type Synonyms type Fn b a = (b :: Type) -> (a :: Type) type ConstFn a b = (a :: Type) -> Fn (b :: Type) (a :: Type) - -Errors -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(37), - ), - expected: 0, - actual: 2, - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(52), - ), - expected: 0, - actual: 1, - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} -CheckError { - kind: TooManyBinders { - signature: Some( - AstId(87), - ), - expected: 1, - actual: 2, - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(7), - t2: Id(8), - }, - crumbs: [ - TermDeclaration( - Idx::(4), - ), - CheckingExpression( - AstId(103), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(7), - t2: Id(8), - }, - crumbs: [ - TermDeclaration( - Idx::(5), - ), - CheckingExpression( - AstId(120), - ), - CheckingExpression( - AstId(123), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.purs b/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.purs new file mode 100644 index 00000000..7596edb0 --- /dev/null +++ b/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.purs @@ -0,0 +1,6 @@ +module Main where + +type UnaryKind = Type -> Type + +data Box :: UnaryKind +data Box a = Box a diff --git a/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.snap b/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.snap new file mode 100644 index 00000000..aca2a0dd --- /dev/null +++ b/tests-integration/fixtures/checking2/144_signature_synonym_data_equation/Main.snap @@ -0,0 +1,17 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: forall (a :: Type). (a :: Type) -> Box (a :: Type) + +Types +UnaryKind :: Type +Box :: UnaryKind + +Synonyms +type UnaryKind = Type -> Type + +Roles +Box = [Representational] diff --git a/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.purs b/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.purs new file mode 100644 index 00000000..1023aa62 --- /dev/null +++ b/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.purs @@ -0,0 +1,7 @@ +module Main where + +type ClassHead = Type -> Constraint + +class EqLike :: ClassHead +class EqLike a where + eqLike :: a -> a -> Boolean diff --git a/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.snap b/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.snap new file mode 100644 index 00000000..55255efa --- /dev/null +++ b/tests-integration/fixtures/checking2/145_signature_synonym_class_equation/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eqLike :: forall (a :: Type). EqLike (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean + +Types +ClassHead :: Type +EqLike :: ClassHead + +Synonyms +type ClassHead = Type -> Constraint + +Classes +class forall (a :: Type). EqLike (a :: Type) + eqLike :: forall (a :: Type). EqLike (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.purs b/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.purs new file mode 100644 index 00000000..3ec4c371 --- /dev/null +++ b/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.purs @@ -0,0 +1,6 @@ +module Main where + +type BinaryKind = Type -> Type -> Type + +type ConstT :: BinaryKind +type ConstT a b = a diff --git a/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.snap b/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.snap new file mode 100644 index 00000000..f3e65ea8 --- /dev/null +++ b/tests-integration/fixtures/checking2/146_signature_synonym_type_equation/Main.snap @@ -0,0 +1,14 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +BinaryKind :: Type +ConstT :: BinaryKind + +Synonyms +type BinaryKind = Type -> Type -> Type +type ConstT a b = (a :: Type) diff --git a/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.purs b/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.purs new file mode 100644 index 00000000..f694f4c5 --- /dev/null +++ b/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.purs @@ -0,0 +1,14 @@ +module Main where + +type Identity :: forall k. k -> k +type Identity a = a + +data Tuple a b = Tuple a b + +test1 :: Identity Array Int +test1 = [42] + +test2 :: Identity Tuple Int String +test2 = Tuple 42 "hello" + +forceSolve = { test1, test2 } diff --git a/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.snap b/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.snap new file mode 100644 index 00000000..5762da42 --- /dev/null +++ b/tests-integration/fixtures/checking2/147_synonym_oversaturation_equations/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +test1 :: Identity @(Type -> Type) Array Int +test2 :: Identity @(Type -> Type -> Type) Tuple Int String +forceSolve :: { test1 :: Array Int, test2 :: Tuple Int String } + +Types +Identity :: forall (k :: Type). (k :: Type) -> (k :: Type) +Tuple :: Type -> Type -> Type + +Synonyms +type Identity a = (a :: (k :: Type)) + +Roles +Tuple = [Representational, Representational] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index f3520cca..fa30712f 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -315,3 +315,11 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_142_const_forms_synonym_forall_main() { run_test("142_const_forms_synonym_forall", "Main"); } #[rustfmt::skip] #[test] fn test_143_const_forms_fn_alias_main() { run_test("143_const_forms_fn_alias", "Main"); } + +#[rustfmt::skip] #[test] fn test_144_signature_synonym_data_equation_main() { run_test("144_signature_synonym_data_equation", "Main"); } + +#[rustfmt::skip] #[test] fn test_145_signature_synonym_class_equation_main() { run_test("145_signature_synonym_class_equation", "Main"); } + +#[rustfmt::skip] #[test] fn test_146_signature_synonym_type_equation_main() { run_test("146_signature_synonym_type_equation", "Main"); } + +#[rustfmt::skip] #[test] fn test_147_synonym_oversaturation_equations_main() { run_test("147_synonym_oversaturation_equations", "Main"); } From 25c78159aa7c753dc2face9df09fc67d691e19a4 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 23:43:56 +0800 Subject: [PATCH 344/386] Implement proper expansion with inspect_signature_core --- .../checking2/src/source/signature.rs | 126 +++++++++++------- .../checking2/src/source/terms/equations.rs | 15 +-- .../checking2/src/source/terms/form_let.rs | 15 +-- .../checking2/src/source/type_items.rs | 42 ++++-- 4 files changed, 118 insertions(+), 80 deletions(-) diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs index 67c0cff6..869b5296 100644 --- a/compiler-core/checking2/src/source/signature.rs +++ b/compiler-core/checking2/src/source/signature.rs @@ -1,11 +1,9 @@ use building_types::QueryResult; -use lowering::TypeVariableBinding; -use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, TypeId, toolkit}; -use crate::error::ErrorKind; +use crate::core::{ForallBinder, Type, TypeId, normalise}; use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; pub struct InspectSignature { pub binders: Vec, @@ -13,69 +11,101 @@ pub struct InspectSignature { pub result: TypeId, } -// TODO: Inline into check_data_equation_check; inspect_quantified and -// inspect_function are still needed for error reporting (mismatch), but -// InspectSignature can be collapsed to just Vec for arguments. -pub fn inspect_signature( +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum InspectMode { + Bindings, + Patterns { required: usize }, +} + +fn inspect_signature_core( state: &mut CheckState, context: &CheckContext, - signature: (lowering::TypeId, TypeId), - bindings: &[TypeVariableBinding], + mut current: TypeId, + mode: InspectMode, ) -> QueryResult where Q: ExternalQueries, { - let (signature_id, signature_kind) = signature; + let mut binders = vec![]; + let mut arguments = vec![]; - let toolkit::InspectQuantified { binders, quantified } = - toolkit::inspect_quantified(state, context, signature_kind)?; + safe_loop! { + current = normalise::normalise_expand(state, context, current)?; - let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function_with(state, context, quantified, toolkit::InspectMode::Full)?; + match context.lookup_type(current) { + Type::Forall(binder_id, inner) => { + binders.push(context.lookup_forall_binder(binder_id)); + current = inner; + } - if bindings.len() > arguments.len() { - state.insert_error(ErrorKind::TypeSignatureVariableMismatch { - id: signature_id, - expected: arguments.len() as u32, - actual: bindings.len() as u32, - }); - } + Type::Constrained(constraint, constrained) => { + if matches!(mode, InspectMode::Patterns { .. }) { + state.push_given(constraint); + current = constrained; + } else { + return Ok(InspectSignature { binders, arguments, result: current }); + } + } + + Type::Function(argument, result) => { + if let InspectMode::Patterns { required } = mode + && arguments.len() >= required + { + return Ok(InspectSignature { binders, arguments, result: current }); + } + + arguments.push(argument); + current = result; + } + + Type::Application(function_argument, result) => { + if let InspectMode::Patterns { required } = mode + && arguments.len() >= required + { + return Ok(InspectSignature { binders, arguments, result: current }); + } - let count = bindings.len().min(arguments.len()); - let arguments = arguments.iter().take(count).copied().collect(); + let function_argument = + normalise::normalise_expand(state, context, function_argument)?; - Ok(InspectSignature { binders, arguments, result }) + let Type::Application(function, argument) = context.lookup_type(function_argument) + else { + return Ok(InspectSignature { binders, arguments, result: current }); + }; + + let function = normalise::normalise_expand(state, context, function)?; + if function == context.prim.function { + arguments.push(argument); + current = result; + } else { + return Ok(InspectSignature { binders, arguments, result: current }); + } + } + + _ => return Ok(InspectSignature { binders, arguments, result: current }), + } + } } pub fn inspect_signature_bindings( state: &mut CheckState, context: &CheckContext, - signature: (lowering::TypeId, TypeId), - bindings: &[TypeVariableBinding], + signature_type: TypeId, ) -> QueryResult where Q: ExternalQueries, { - let (signature_id, signature_kind) = signature; - - let toolkit::InspectQuantified { binders, quantified } = - toolkit::inspect_quantified(state, context, signature_kind)?; - - let toolkit::InspectFunction { arguments, result } = - toolkit::inspect_function_with(state, context, quantified, toolkit::InspectMode::Full)?; - - if bindings.len() > arguments.len() { - state.insert_error(ErrorKind::TypeSignatureVariableMismatch { - id: signature_id, - expected: arguments.len() as u32, - actual: bindings.len() as u32, - }); - } - - let mut remaining = arguments.into_iter(); - - let arguments = remaining.by_ref().take(bindings.len()).collect(); - let result = context.intern_function_chain_iter(remaining, result); + inspect_signature_core(state, context, signature_type, InspectMode::Bindings) +} - Ok(InspectSignature { binders, arguments, result }) +pub fn inspect_signature_patterns( + state: &mut CheckState, + context: &CheckContext, + signature_type: TypeId, + required: usize, +) -> QueryResult +where + Q: ExternalQueries, +{ + inspect_signature_core(state, context, signature_type, InspectMode::Patterns { required }) } diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index e92293af..a0360e44 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -7,7 +7,7 @@ use crate::context::CheckContext; use crate::core::{TypeId, exhaustive, toolkit, unification}; use crate::error::ErrorKind; use crate::source::terms::form_let; -use crate::source::{binder, terms}; +use crate::source::{binder, signature, terms}; use crate::state::CheckState; pub enum EquationTypeOrigin { @@ -27,17 +27,8 @@ where { let required = equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, expected_type)?; - - let quantified = toolkit::collect_givens(state, context, quantified)?; - - let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function_with( - state, - context, - quantified, - toolkit::InspectMode::Some(required), - )?; + let signature::InspectSignature { arguments, result, .. } = + signature::inspect_signature_patterns(state, context, expected_type, required)?; let function = context.intern_function_chain(&arguments, result); check_equations_core(state, context, origin, &arguments, result, function, equations)?; diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 39a0f839..53a63477 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -5,7 +5,7 @@ use crate::context::CheckContext; use crate::core::{exhaustive, toolkit}; use crate::error::ErrorCrumb; use crate::source::terms::equations; -use crate::source::{binder, types}; +use crate::source::{binder, signature, types}; use crate::state::CheckState; pub fn check_let_chunks( @@ -142,17 +142,8 @@ where let required = name.equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, name_type)?; - - let quantified = toolkit::collect_givens(state, context, quantified)?; - - let toolkit::InspectFunction { arguments, result } = toolkit::inspect_function_with( - state, - context, - quantified, - toolkit::InspectMode::Some(required), - )?; + let signature::InspectSignature { arguments, result, .. } = + signature::inspect_signature_patterns(state, context, name_type, required)?; let function = context.intern_function_chain(&arguments, result); diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index f9a20027..0c46010c 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -309,10 +309,40 @@ fn check_data_equation_check( where Q: ExternalQueries, { - let signature = signature::inspect_signature_bindings(state, context, signature, bindings)?; + let signature = expect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } +fn expect_signature_bindings( + state: &mut CheckState, + context: &CheckContext, + (signature_id, signature_kind): (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult +where + Q: ExternalQueries, +{ + let signature = signature::inspect_signature_bindings(state, context, signature_kind)?; + + let actual = bindings.len() as u32; + let expected = signature.arguments.len() as u32; + + if actual > expected { + state.insert_error(ErrorKind::TypeSignatureVariableMismatch { + id: signature_id, + expected, + actual, + }); + } + + let mut remaining = signature.arguments.into_iter(); + + let arguments = remaining.by_ref().take(actual as usize).collect(); + let result = context.intern_function_chain_iter(remaining, signature.result); + + Ok(signature::InspectSignature { binders: signature.binders, arguments, result }) +} + fn check_type_variable_bindings( state: &mut CheckState, context: &CheckContext, @@ -601,12 +631,8 @@ fn check_synonym_equation_check( where Q: ExternalQueries, { - let signature = signature::inspect_signature_bindings( - state, - context, - (signature_id, signature_kind), - bindings, - )?; + let signature = + expect_signature_bindings(state, context, (signature_id, signature_kind), bindings)?; let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; Ok((parameters, signature_kind, signature.result)) } @@ -698,7 +724,7 @@ fn check_class_equation_check( where Q: ExternalQueries, { - let signature = signature::inspect_signature_bindings(state, context, signature, bindings)?; + let signature = expect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } From a866978366348b0aeec9539f86c92e75e6894439 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 5 Mar 2026 22:48:32 +0800 Subject: [PATCH 345/386] Add ParsedSynonym struct --- compiler-core/checking2/src/error.rs | 2 +- compiler-core/checking2/src/source/synonym.rs | 32 +++++++++++++------ compiler-core/checking2/src/source/types.rs | 6 ++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/compiler-core/checking2/src/error.rs b/compiler-core/checking2/src/error.rs index a9d5aa57..39f4a302 100644 --- a/compiler-core/checking2/src/error.rs +++ b/compiler-core/checking2/src/error.rs @@ -105,7 +105,7 @@ pub enum ErrorKind { }, RecursiveSynonymExpansion { file_id: files::FileId, - item_id: indexing::TypeItemId, + type_id: indexing::TypeItemId, }, TooManyBinders { signature: Option, diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index e53d8537..d4dd8e83 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -15,11 +15,19 @@ use crate::error::ErrorKind; use crate::source::types; use crate::state::CheckState; -pub fn parse_synonym_application( +#[derive(Debug, Clone, Copy)] +pub struct ParsedSynonym { + pub file_id: FileId, + pub type_id: TypeItemId, + pub kind: TypeId, + pub arity: usize, +} + +pub fn parse_synonym( state: &mut CheckState, context: &CheckContext, function: lowering::TypeId, -) -> QueryResult> +) -> QueryResult> where Q: ExternalQueries, { @@ -41,21 +49,23 @@ where let kind = checked_synonym.kind; let arity = checked_synonym.parameters.len(); - Ok(Some((file_id, type_id, kind, arity))) + Ok(Some(ParsedSynonym { file_id, type_id, kind, arity })) } pub fn infer_synonym_constructor( state: &mut CheckState, context: &CheckContext, - (file_id, item_id, kind, arity): (FileId, TypeItemId, TypeId, usize), + synonym: ParsedSynonym, id: lowering::TypeId, ) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { + let ParsedSynonym { file_id, type_id, kind, arity } = synonym; + if arity > 0 { if state.defer_expansion { - let synonym_type = context.queries.intern_type(Type::Constructor(file_id, item_id)); + let synonym_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); return Ok((synonym_type, kind)); } @@ -64,13 +74,13 @@ where return Ok((unknown, unknown)); } - if is_recursive_synonym(context, file_id, item_id)? { - state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id }); + if is_recursive_synonym(context, file_id, type_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, type_id }); } let synonym = Synonym { saturation: Saturation::Full, - reference: (file_id, item_id), + reference: (file_id, type_id), arguments: Arc::default(), }; @@ -84,14 +94,16 @@ pub fn infer_synonym_application( state: &mut CheckState, context: &CheckContext, id: lowering::TypeId, - (file_id, type_id, function_kind, arity): (FileId, TypeItemId, TypeId, usize), + synonym: ParsedSynonym, arguments: &[lowering::TypeId], ) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { + let ParsedSynonym { file_id, type_id, kind: function_kind, arity } = synonym; + if is_recursive_synonym(context, file_id, type_id)? { - state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, item_id: type_id }); + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, type_id }); } if arguments.len() < arity { diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 9d606e49..5183c189 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -87,7 +87,7 @@ where return Ok(unknown("missing application function")); }; - if let Some(synonym) = synonym::parse_synonym_application(state, context, *function)? { + if let Some(synonym) = synonym::parse_synonym(state, context, *function)? { return synonym::infer_synonym_application(state, context, id, synonym, arguments); } @@ -148,10 +148,10 @@ where return Ok(unknown("missing constructor")); }; - if let Some(synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? { - let synonym = (file_id, type_id, synonym.kind, synonym.parameters.len()); + if let Some(synonym) = synonym::parse_synonym(state, context, id)? { return synonym::infer_synonym_constructor(state, context, synonym, id); } + let t = context.queries.intern_type(Type::Constructor(file_id, type_id)); let k = toolkit::lookup_file_type(state, context, file_id, type_id)?; From 101eca891378d1d7589925880f40707a981b90ca Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 03:03:09 +0800 Subject: [PATCH 346/386] Implement prenex-aware decompose_signature --- compiler-core/checking2/src/core.rs | 1 + compiler-core/checking2/src/core/signature.rs | 150 ++++++++++++++++++ compiler-core/checking2/src/source.rs | 1 - .../checking2/src/source/signature.rs | 111 ------------- .../checking2/src/source/terms/equations.rs | 8 +- .../checking2/src/source/terms/form_let.rs | 8 +- .../checking2/src/source/type_items.rs | 46 ++---- 7 files changed, 169 insertions(+), 156 deletions(-) create mode 100644 compiler-core/checking2/src/core/signature.rs delete mode 100644 compiler-core/checking2/src/source/signature.rs diff --git a/compiler-core/checking2/src/core.rs b/compiler-core/checking2/src/core.rs index 8ecb0d75..3d91df23 100644 --- a/compiler-core/checking2/src/core.rs +++ b/compiler-core/checking2/src/core.rs @@ -6,6 +6,7 @@ pub mod fold; pub mod generalise; pub mod normalise; pub mod pretty; +pub mod signature; pub mod substitute; pub mod toolkit; pub mod unification; diff --git a/compiler-core/checking2/src/core/signature.rs b/compiler-core/checking2/src/core/signature.rs new file mode 100644 index 00000000..64f249e5 --- /dev/null +++ b/compiler-core/checking2/src/core/signature.rs @@ -0,0 +1,150 @@ +use building_types::QueryResult; +use lowering::TypeVariableBinding; + +use crate::context::CheckContext; +use crate::core::{ForallBinder, Type, TypeId, normalise}; +use crate::error::ErrorKind; +use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; + +pub struct DecomposedSignature { + pub binders: Vec, + pub constraints: Vec, + pub arguments: Vec, + pub result: TypeId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecomposeSignatureMode { + Bindings, + Patterns { required: usize }, +} + +pub fn decompose_signature( + state: &mut CheckState, + context: &CheckContext, + mut current: TypeId, + mode: DecomposeSignatureMode, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut binders = vec![]; + let mut constraints = vec![]; + let mut arguments = vec![]; + + safe_loop! { + current = normalise::normalise_expand(state, context, current)?; + + match context.lookup_type(current) { + Type::Forall(binder_id, inner) => { + binders.push(context.lookup_forall_binder(binder_id)); + current = inner; + } + + Type::Constrained(constraint, constrained) => { + if matches!(mode, DecomposeSignatureMode::Patterns { .. }) { + constraints.push(constraint); + current = constrained; + } else { + return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); + } + } + + Type::Function(argument, result) => { + if let DecomposeSignatureMode::Patterns { required } = mode + && arguments.len() >= required + { + return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); + } + + arguments.push(argument); + current = result; + } + + Type::Application(function_argument, result) => { + if let DecomposeSignatureMode::Patterns { required } = mode + && arguments.len() >= required + { + return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); + } + + let function_argument = + normalise::normalise_expand(state, context, function_argument)?; + + let Type::Application(function, argument) = context.lookup_type(function_argument) + else { + return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); + }; + + let function = normalise::normalise_expand(state, context, function)?; + if function == context.prim.function { + arguments.push(argument); + current = result; + } else { + return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); + } + } + + _ => return Ok(DecomposedSignature { binders, constraints, arguments, result: current }), + } + } +} + +pub fn expect_signature_bindings( + state: &mut CheckState, + context: &CheckContext, + (signature_id, signature_type): (lowering::TypeId, TypeId), + bindings: &[TypeVariableBinding], +) -> QueryResult +where + Q: ExternalQueries, +{ + let signature = + decompose_signature(state, context, signature_type, DecomposeSignatureMode::Bindings)?; + + let actual = bindings.len() as u32; + let expected = signature.arguments.len() as u32; + + if actual > expected { + state.insert_error(ErrorKind::TypeSignatureVariableMismatch { + id: signature_id, + expected, + actual, + }); + } + + let mut remaining = signature.arguments.into_iter(); + let arguments = remaining.by_ref().take(actual as usize).collect(); + let result = context.intern_function_chain_iter(remaining, signature.result); + + Ok(DecomposedSignature { + binders: signature.binders, + constraints: signature.constraints, + arguments, + result, + }) +} + +pub fn expect_signature_patterns( + state: &mut CheckState, + context: &CheckContext, + signature_type: TypeId, + required: usize, +) -> QueryResult +where + Q: ExternalQueries, +{ + let signature = decompose_signature( + state, + context, + signature_type, + DecomposeSignatureMode::Patterns { required }, + )?; + + for &constraint in &signature.constraints { + state.push_given(constraint); + } + + Ok(signature) +} diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 22e1493b..a8ef82e8 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -3,7 +3,6 @@ pub mod binder; pub mod operator; pub mod roles; -pub mod signature; pub mod synonym; pub mod terms; pub mod types; diff --git a/compiler-core/checking2/src/source/signature.rs b/compiler-core/checking2/src/source/signature.rs deleted file mode 100644 index 869b5296..00000000 --- a/compiler-core/checking2/src/source/signature.rs +++ /dev/null @@ -1,111 +0,0 @@ -use building_types::QueryResult; - -use crate::context::CheckContext; -use crate::core::{ForallBinder, Type, TypeId, normalise}; -use crate::state::CheckState; -use crate::{ExternalQueries, safe_loop}; - -pub struct InspectSignature { - pub binders: Vec, - pub arguments: Vec, - pub result: TypeId, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum InspectMode { - Bindings, - Patterns { required: usize }, -} - -fn inspect_signature_core( - state: &mut CheckState, - context: &CheckContext, - mut current: TypeId, - mode: InspectMode, -) -> QueryResult -where - Q: ExternalQueries, -{ - let mut binders = vec![]; - let mut arguments = vec![]; - - safe_loop! { - current = normalise::normalise_expand(state, context, current)?; - - match context.lookup_type(current) { - Type::Forall(binder_id, inner) => { - binders.push(context.lookup_forall_binder(binder_id)); - current = inner; - } - - Type::Constrained(constraint, constrained) => { - if matches!(mode, InspectMode::Patterns { .. }) { - state.push_given(constraint); - current = constrained; - } else { - return Ok(InspectSignature { binders, arguments, result: current }); - } - } - - Type::Function(argument, result) => { - if let InspectMode::Patterns { required } = mode - && arguments.len() >= required - { - return Ok(InspectSignature { binders, arguments, result: current }); - } - - arguments.push(argument); - current = result; - } - - Type::Application(function_argument, result) => { - if let InspectMode::Patterns { required } = mode - && arguments.len() >= required - { - return Ok(InspectSignature { binders, arguments, result: current }); - } - - let function_argument = - normalise::normalise_expand(state, context, function_argument)?; - - let Type::Application(function, argument) = context.lookup_type(function_argument) - else { - return Ok(InspectSignature { binders, arguments, result: current }); - }; - - let function = normalise::normalise_expand(state, context, function)?; - if function == context.prim.function { - arguments.push(argument); - current = result; - } else { - return Ok(InspectSignature { binders, arguments, result: current }); - } - } - - _ => return Ok(InspectSignature { binders, arguments, result: current }), - } - } -} - -pub fn inspect_signature_bindings( - state: &mut CheckState, - context: &CheckContext, - signature_type: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - inspect_signature_core(state, context, signature_type, InspectMode::Bindings) -} - -pub fn inspect_signature_patterns( - state: &mut CheckState, - context: &CheckContext, - signature_type: TypeId, - required: usize, -) -> QueryResult -where - Q: ExternalQueries, -{ - inspect_signature_core(state, context, signature_type, InspectMode::Patterns { required }) -} diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index a0360e44..de8142f3 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -4,10 +4,10 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, exhaustive, toolkit, unification}; +use crate::core::{TypeId, exhaustive, signature, toolkit, unification}; use crate::error::ErrorKind; use crate::source::terms::form_let; -use crate::source::{binder, signature, terms}; +use crate::source::{binder, terms}; use crate::state::CheckState; pub enum EquationTypeOrigin { @@ -27,8 +27,8 @@ where { let required = equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - let signature::InspectSignature { arguments, result, .. } = - signature::inspect_signature_patterns(state, context, expected_type, required)?; + let signature::DecomposedSignature { arguments, result, .. } = + signature::expect_signature_patterns(state, context, expected_type, required)?; let function = context.intern_function_chain(&arguments, result); check_equations_core(state, context, origin, &arguments, result, function, equations)?; diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 53a63477..37e83d90 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -2,10 +2,10 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{exhaustive, toolkit}; +use crate::core::{exhaustive, signature, toolkit}; use crate::error::ErrorCrumb; use crate::source::terms::equations; -use crate::source::{binder, signature, types}; +use crate::source::{binder, types}; use crate::state::CheckState; pub fn check_let_chunks( @@ -142,8 +142,8 @@ where let required = name.equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - let signature::InspectSignature { arguments, result, .. } = - signature::inspect_signature_patterns(state, context, name_type, required)?; + let signature::DecomposedSignature { arguments, result, .. } = + signature::expect_signature_patterns(state, context, name_type, required)?; let function = context.intern_function_chain(&arguments, result); diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 0c46010c..65d3017f 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -14,11 +14,11 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::{ - CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, generalise, toolkit, + CheckedClass, CheckedSynonym, ForallBinder, Role, Type, TypeId, generalise, signature, toolkit, unification, zonk, }; use crate::error::{ErrorCrumb, ErrorKind}; -use crate::source::{signature, types}; +use crate::source::types; use crate::state::CheckState; struct PendingDataType { @@ -309,40 +309,10 @@ fn check_data_equation_check( where Q: ExternalQueries, { - let signature = expect_signature_bindings(state, context, signature, bindings)?; + let signature = signature::expect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } -fn expect_signature_bindings( - state: &mut CheckState, - context: &CheckContext, - (signature_id, signature_kind): (lowering::TypeId, TypeId), - bindings: &[TypeVariableBinding], -) -> QueryResult -where - Q: ExternalQueries, -{ - let signature = signature::inspect_signature_bindings(state, context, signature_kind)?; - - let actual = bindings.len() as u32; - let expected = signature.arguments.len() as u32; - - if actual > expected { - state.insert_error(ErrorKind::TypeSignatureVariableMismatch { - id: signature_id, - expected, - actual, - }); - } - - let mut remaining = signature.arguments.into_iter(); - - let arguments = remaining.by_ref().take(actual as usize).collect(); - let result = context.intern_function_chain_iter(remaining, signature.result); - - Ok(signature::InspectSignature { binders: signature.binders, arguments, result }) -} - fn check_type_variable_bindings( state: &mut CheckState, context: &CheckContext, @@ -631,8 +601,12 @@ fn check_synonym_equation_check( where Q: ExternalQueries, { - let signature = - expect_signature_bindings(state, context, (signature_id, signature_kind), bindings)?; + let signature = signature::expect_signature_bindings( + state, + context, + (signature_id, signature_kind), + bindings, + )?; let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; Ok((parameters, signature_kind, signature.result)) } @@ -724,7 +698,7 @@ fn check_class_equation_check( where Q: ExternalQueries, { - let signature = expect_signature_bindings(state, context, signature, bindings)?; + let signature = signature::expect_signature_bindings(state, context, signature, bindings)?; check_type_variable_bindings(state, context, bindings, &signature.arguments) } From e5c2e1c804bd2daf9336324195db2a5bb89d97e3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 04:05:34 +0800 Subject: [PATCH 347/386] Migrate inspect_ function calls to use decompose_signature --- compiler-core/checking2/src/core/signature.rs | 12 +++----- compiler-core/checking2/src/source.rs | 14 ++++----- .../checking2/src/source/derive/head.rs | 14 +++++---- compiler-core/checking2/src/source/roles.rs | 12 ++++---- .../checking2/src/source/term_items.rs | 13 ++++---- .../checking2/src/source/type_items.rs | 30 ++++++++++++------- 6 files changed, 55 insertions(+), 40 deletions(-) diff --git a/compiler-core/checking2/src/core/signature.rs b/compiler-core/checking2/src/core/signature.rs index 64f249e5..8cecda89 100644 --- a/compiler-core/checking2/src/core/signature.rs +++ b/compiler-core/checking2/src/core/signature.rs @@ -16,7 +16,7 @@ pub struct DecomposedSignature { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DecomposeSignatureMode { - Bindings, + Full, Patterns { required: usize }, } @@ -43,12 +43,8 @@ where } Type::Constrained(constraint, constrained) => { - if matches!(mode, DecomposeSignatureMode::Patterns { .. }) { - constraints.push(constraint); - current = constrained; - } else { - return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); - } + constraints.push(constraint); + current = constrained; } Type::Function(argument, result) => { @@ -101,7 +97,7 @@ where Q: ExternalQueries, { let signature = - decompose_signature(state, context, signature_type, DecomposeSignatureMode::Bindings)?; + decompose_signature(state, context, signature_type, DecomposeSignatureMode::Full)?; let actual = bindings.len() as u32; let expected = signature.arguments.len() as u32; diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index a8ef82e8..43766c69 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -18,7 +18,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{TypeId, toolkit}; +use crate::core::{TypeId, signature}; use crate::state::CheckState; fn is_binary_operator_type( @@ -29,12 +29,12 @@ fn is_binary_operator_type( where Q: ExternalQueries, { - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, kind)?; - let quantified = toolkit::without_constraints(state, context, quantified)?; - - let toolkit::InspectFunction { arguments, .. } = - toolkit::inspect_function(state, context, quantified)?; + let signature::DecomposedSignature { arguments, .. } = signature::decompose_signature( + state, + context, + kind, + signature::DecomposeSignatureMode::Full, + )?; Ok(arguments.len() == 2) } diff --git a/compiler-core/checking2/src/source/derive/head.rs b/compiler-core/checking2/src/source/derive/head.rs index 6b6fa1bb..2df9e480 100644 --- a/compiler-core/checking2/src/source/derive/head.rs +++ b/compiler-core/checking2/src/source/derive/head.rs @@ -5,7 +5,9 @@ use lowering::TermItemIr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{CheckedInstance, Type, constraint, generalise, toolkit, unification, zonk}; +use crate::core::{ + CheckedInstance, Type, constraint, generalise, signature, toolkit, unification, zonk, +}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::types; use crate::state::CheckState; @@ -126,10 +128,12 @@ where let class_kind = toolkit::lookup_file_type(state, context, class_file, class_id)?; let expected_kinds = { - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, class_kind)?; - let toolkit::InspectFunction { arguments, .. } = - toolkit::inspect_function(state, context, quantified)?; + let signature::DecomposedSignature { arguments, .. } = signature::decompose_signature( + state, + context, + class_kind, + signature::DecomposeSignatureMode::Full, + )?; arguments }; diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index caefa512..00e50bfb 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -8,7 +8,7 @@ use rustc_hash::FxHashMap; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{ForallBinder, KindOrType, Name, Role, Type, TypeId, normalise, toolkit}; +use crate::core::{ForallBinder, KindOrType, Name, Role, Type, TypeId, normalise, signature}; use crate::error::{ErrorCrumb, ErrorKind}; use crate::state::CheckState; @@ -40,10 +40,12 @@ pub fn count_kind_arguments( where Q: ExternalQueries, { - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, kind)?; - let toolkit::InspectFunction { arguments, .. } = - toolkit::inspect_function(state, context, quantified)?; + let signature::DecomposedSignature { arguments, .. } = signature::decompose_signature( + state, + context, + kind, + signature::DecomposeSignatureMode::Full, + )?; Ok(arguments.len()) } diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 18b0d443..91df140f 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -10,7 +10,8 @@ use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::{NameToType, SubstituteName}; use crate::core::{ - CheckedInstance, Type, TypeId, constraint, generalise, normalise, toolkit, unification, zonk, + CheckedInstance, Type, TypeId, constraint, generalise, normalise, signature, toolkit, + unification, zonk, }; use crate::error::{ErrorCrumb, ErrorKind}; use crate::source::terms::equations; @@ -95,10 +96,12 @@ where let class_kind = toolkit::lookup_file_type(state, context, class_file, class_id)?; let expected_kinds = { - let toolkit::InspectQuantified { quantified, .. } = - toolkit::inspect_quantified(state, context, class_kind)?; - let toolkit::InspectFunction { arguments, .. } = - toolkit::inspect_function(state, context, quantified)?; + let signature::DecomposedSignature { arguments, .. } = signature::decompose_signature( + state, + context, + class_kind, + signature::DecomposeSignatureMode::Full, + )?; arguments }; diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 65d3017f..d4aa1159 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -449,11 +449,16 @@ where continue; }; - let toolkit::InspectQuantified { binders: kind_binders, quantified } = - toolkit::inspect_quantified(state, context, constructor_kind)?; - - let toolkit::InspectFunction { arguments: parameter_kinds, .. } = - toolkit::inspect_function(state, context, quantified)?; + let signature::DecomposedSignature { + binders: kind_binders, + arguments: parameter_kinds, + .. + } = signature::decompose_signature( + state, + context, + constructor_kind, + signature::DecomposeSignatureMode::Full, + )?; // parameter_kinds is the post-generalisation kind for each parameter; // we want to replace pre-generalisation kinds carried by parameters @@ -766,11 +771,16 @@ where continue; }; - let toolkit::InspectQuantified { binders: class_binders, quantified: class_inner } = - toolkit::inspect_quantified(state, context, class_kind)?; - - let toolkit::InspectFunction { arguments: class_parameters, .. } = - toolkit::inspect_function(state, context, class_inner)?; + let signature::DecomposedSignature { + binders: class_binders, + arguments: class_parameters, + .. + } = signature::decompose_signature( + state, + context, + class_kind, + signature::DecomposeSignatureMode::Full, + )?; let get_parameter_kind = |index: usize| { if let Some(kind) = class_parameters.get(index) { From a9748ac0a0526f90bf5537b9b8c138b65708d7d5 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 04:15:17 +0800 Subject: [PATCH 348/386] Implement prenex awareness for class members --- .../checking2/src/source/term_items.rs | 50 +++++++++++++++++-- .../checking2/src/source/type_items.rs | 21 ++++++-- .../148_class_member_prenex/Main.purs | 5 ++ .../148_class_member_prenex/Main.snap | 24 +++++++++ .../tests/checking2/generated.rs | 2 + 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests-integration/fixtures/checking2/148_class_member_prenex/Main.purs create mode 100644 tests-integration/fixtures/checking2/148_class_member_prenex/Main.snap diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 91df140f..a75b9efa 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -341,8 +341,13 @@ where return Ok(None); }; - let toolkit::InspectQuantified { binders, quantified } = - toolkit::inspect_quantified(state, context, class_member_type)?; + let signature::DecomposedSignature { binders, constraints, arguments, result } = + signature::decompose_signature( + state, + context, + class_member_type, + signature::DecomposeSignatureMode::Full, + )?; let class_binder_count = class_info.kind_binders.len() + class_info.type_parameters.len(); if binders.len() < class_binder_count @@ -363,10 +368,12 @@ where bindings.insert(binder.name, argument); } - let mut specialised = SubstituteName::many(state, context, &bindings, quantified)?; - specialised = normalise::normalise(state, context, specialised)?; + let constraints = substitute_normalise_types(state, context, &bindings, &constraints)?; + let arguments = substitute_normalise_types(state, context, &bindings, &arguments)?; + let mut constrained = substitute_normalise_type(state, context, &bindings, result)?; - let Type::Constrained(constraint, mut constrained) = context.lookup_type(specialised) else { + let mut constraints = constraints.into_iter(); + let Some(constraint) = constraints.next() else { return Ok(None); }; @@ -378,6 +385,11 @@ where return Ok(None); } + constrained = context.intern_function_chain(&arguments, constrained); + for constraint in constraints.rev() { + constrained = context.intern_constrained(constraint, constrained); + } + for binder in member_binders.iter().rev() { let binder_id = context.intern_forall_binder(*binder); constrained = context.intern_forall(binder_id, constrained); @@ -386,6 +398,34 @@ where Ok(Some(constrained)) } +fn substitute_normalise_type( + state: &mut CheckState, + context: &CheckContext, + bindings: &NameToType, + type_id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let type_id = SubstituteName::many(state, context, bindings, type_id)?; + normalise::normalise(state, context, type_id) +} + +fn substitute_normalise_types( + state: &mut CheckState, + context: &CheckContext, + bindings: &NameToType, + types: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + types + .iter() + .map(|&type_id| substitute_normalise_type(state, context, bindings, type_id)) + .collect() +} + fn prepare_binding_group(state: &mut CheckState, context: &CheckContext, items: &[TermItemId]) where Q: ExternalQueries, diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index d4aa1159..7af3010f 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -836,10 +836,25 @@ where for (member_id, member_type) in members.iter() { let member_type = zonk::zonk(state, context, *member_type)?; - let toolkit::InspectQuantified { binders: member_binders, quantified: member_inner } = - toolkit::inspect_quantified(state, context, member_type)?; + let signature::DecomposedSignature { + binders: member_binders, + constraints: member_constraints, + arguments: member_arguments, + result: member_result, + } = signature::decompose_signature( + state, + context, + member_type, + signature::DecomposeSignatureMode::Full, + )?; + + let mut result = context.intern_function_chain(&member_arguments, member_result); + + for constraint in member_constraints.into_iter().rev() { + result = context.intern_constrained(constraint, result); + } - let mut result = context.intern_constrained(class_head, member_inner); + result = context.intern_constrained(class_head, result); for member_binder in member_binders.iter().copied().rev() { let binder_id = context.intern_forall_binder(member_binder); diff --git a/tests-integration/fixtures/checking2/148_class_member_prenex/Main.purs b/tests-integration/fixtures/checking2/148_class_member_prenex/Main.purs new file mode 100644 index 00000000..2ec9a4c7 --- /dev/null +++ b/tests-integration/fixtures/checking2/148_class_member_prenex/Main.purs @@ -0,0 +1,5 @@ +module Main where + +class Const :: (Type -> Type) -> Constraint +class Const f where + const :: forall a. f a -> forall b. f b -> f a diff --git a/tests-integration/fixtures/checking2/148_class_member_prenex/Main.snap b/tests-integration/fixtures/checking2/148_class_member_prenex/Main.snap new file mode 100644 index 00000000..3b36c82c --- /dev/null +++ b/tests-integration/fixtures/checking2/148_class_member_prenex/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +const :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Const (f :: Type -> Type) => + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) -> + (f :: Type -> Type) (a :: Type) + +Types +Const :: (Type -> Type) -> Constraint + +Classes +class forall (f :: Type -> Type). Const (f :: Type -> Type) + const :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Const (f :: Type -> Type) => + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) -> + (f :: Type -> Type) (a :: Type) diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index fa30712f..cbc35119 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -323,3 +323,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_146_signature_synonym_type_equation_main() { run_test("146_signature_synonym_type_equation", "Main"); } #[rustfmt::skip] #[test] fn test_147_synonym_oversaturation_equations_main() { run_test("147_synonym_oversaturation_equations", "Main"); } + +#[rustfmt::skip] #[test] fn test_148_class_member_prenex_main() { run_test("148_class_member_prenex", "Main"); } From d0733b061f0ec258678026bef24ffa1d9a950f0c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 04:45:31 +0800 Subject: [PATCH 349/386] Remove super from derive functions --- .../checking2/src/source/derive/contravariant.rs | 4 ++-- compiler-core/checking2/src/source/derive/eq1_ord1.rs | 4 ++-- compiler-core/checking2/src/source/derive/eq_ord.rs | 4 ++-- compiler-core/checking2/src/source/derive/field.rs | 2 +- compiler-core/checking2/src/source/derive/foldable.rs | 4 ++-- compiler-core/checking2/src/source/derive/functor.rs | 4 ++-- compiler-core/checking2/src/source/derive/generic.rs | 2 +- compiler-core/checking2/src/source/derive/newtype.rs | 4 ++-- compiler-core/checking2/src/source/derive/tools.rs | 8 ++++---- compiler-core/checking2/src/source/derive/traversable.rs | 4 ++-- compiler-core/checking2/src/source/derive/variance.rs | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/compiler-core/checking2/src/source/derive/contravariant.rs b/compiler-core/checking2/src/source/derive/contravariant.rs index cac30a0f..7404d4f2 100644 --- a/compiler-core/checking2/src/source/derive/contravariant.rs +++ b/compiler-core/checking2/src/source/derive/contravariant.rs @@ -11,7 +11,7 @@ use crate::state::CheckState; use super::DeriveStrategy; use super::variance::{Variance, VarianceConfig}; -pub(super) fn check_derive_contravariant( +pub fn check_derive_contravariant( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -50,7 +50,7 @@ where })) } -pub(super) fn check_derive_profunctor( +pub fn check_derive_profunctor( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/eq1_ord1.rs b/compiler-core/checking2/src/source/derive/eq1_ord1.rs index 7d8fdc56..69040ee2 100644 --- a/compiler-core/checking2/src/source/derive/eq1_ord1.rs +++ b/compiler-core/checking2/src/source/derive/eq1_ord1.rs @@ -10,7 +10,7 @@ use crate::state::CheckState; use super::DeriveStrategy; -pub(super) fn check_derive_eq1( +pub fn check_derive_eq1( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -28,7 +28,7 @@ where check_derive_delegate_constraint(state, context, class_file, class_id, arguments, eq) } -pub(super) fn check_derive_ord1( +pub fn check_derive_ord1( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/eq_ord.rs b/compiler-core/checking2/src/source/derive/eq_ord.rs index 13ba9533..2eefb392 100644 --- a/compiler-core/checking2/src/source/derive/eq_ord.rs +++ b/compiler-core/checking2/src/source/derive/eq_ord.rs @@ -10,7 +10,7 @@ use crate::state::CheckState; use super::DeriveStrategy; -pub(super) fn check_derive_eq( +pub fn check_derive_eq( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -23,7 +23,7 @@ where check_derive_field_constraints(state, context, class_file, class_id, arguments) } -pub(super) fn check_derive_ord( +pub fn check_derive_ord( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs index 99bad668..dcd95a4b 100644 --- a/compiler-core/checking2/src/source/derive/field.rs +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -10,7 +10,7 @@ use crate::state::CheckState; use super::tools; -pub(super) fn generate_field_constraints( +pub fn generate_field_constraints( state: &mut CheckState, context: &CheckContext, data_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/foldable.rs b/compiler-core/checking2/src/source/derive/foldable.rs index ecb2b506..8f1f382a 100644 --- a/compiler-core/checking2/src/source/derive/foldable.rs +++ b/compiler-core/checking2/src/source/derive/foldable.rs @@ -11,7 +11,7 @@ use crate::state::CheckState; use super::DeriveStrategy; use super::variance::{Variance, VarianceConfig}; -pub(super) fn check_derive_foldable( +pub fn check_derive_foldable( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -50,7 +50,7 @@ where })) } -pub(super) fn check_derive_bifoldable( +pub fn check_derive_bifoldable( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/functor.rs b/compiler-core/checking2/src/source/derive/functor.rs index 0ffdc742..1674b099 100644 --- a/compiler-core/checking2/src/source/derive/functor.rs +++ b/compiler-core/checking2/src/source/derive/functor.rs @@ -11,7 +11,7 @@ use crate::state::CheckState; use super::DeriveStrategy; use super::variance::{Variance, VarianceConfig}; -pub(super) fn check_derive_functor( +pub fn check_derive_functor( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -50,7 +50,7 @@ where })) } -pub(super) fn check_derive_bifunctor( +pub fn check_derive_bifunctor( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs index 85854363..8f686d09 100644 --- a/compiler-core/checking2/src/source/derive/generic.rs +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -13,7 +13,7 @@ use crate::{ExternalQueries, safe_loop}; use super::{DeriveStrategy, tools}; -pub(super) fn check_derive_generic( +pub fn check_derive_generic( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs index 9817bc8e..a2c4b2a4 100644 --- a/compiler-core/checking2/src/source/derive/newtype.rs +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -10,7 +10,7 @@ use crate::state::CheckState; use super::DeriveStrategy; -pub(super) fn check_derive_newtype( +pub fn check_derive_newtype( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -56,7 +56,7 @@ where Ok(Some(DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint })) } -pub(super) fn check_derive_newtype_class( +pub fn check_derive_newtype_class( state: &mut CheckState, context: &CheckContext, _class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/tools.rs b/compiler-core/checking2/src/source/derive/tools.rs index 1cdfcd6f..462329da 100644 --- a/compiler-core/checking2/src/source/derive/tools.rs +++ b/compiler-core/checking2/src/source/derive/tools.rs @@ -10,7 +10,7 @@ use crate::core::{CheckedClass, Type, TypeId, toolkit}; use crate::error::ErrorKind; use crate::state::CheckState; -pub(super) fn emit_constraint( +pub fn emit_constraint( context: &CheckContext, state: &mut CheckState, class: (FileId, TypeItemId), @@ -22,7 +22,7 @@ pub(super) fn emit_constraint( state.push_wanted(context.intern_application(class_t, argument)); } -pub(super) fn emit_superclass_constraints( +pub fn emit_superclass_constraints( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -56,7 +56,7 @@ where Ok(()) } -pub(super) fn lookup_data_constructors( +pub fn lookup_data_constructors( context: &CheckContext, data_file: FileId, data_id: TypeItemId, @@ -72,7 +72,7 @@ where } } -pub(super) fn solve_and_report_constraints( +pub fn solve_and_report_constraints( state: &mut CheckState, context: &CheckContext, ) -> QueryResult<()> diff --git a/compiler-core/checking2/src/source/derive/traversable.rs b/compiler-core/checking2/src/source/derive/traversable.rs index 606d4491..ec40f283 100644 --- a/compiler-core/checking2/src/source/derive/traversable.rs +++ b/compiler-core/checking2/src/source/derive/traversable.rs @@ -11,7 +11,7 @@ use crate::state::CheckState; use super::DeriveStrategy; use super::variance::{Variance, VarianceConfig}; -pub(super) fn check_derive_traversable( +pub fn check_derive_traversable( state: &mut CheckState, context: &CheckContext, class_file: FileId, @@ -50,7 +50,7 @@ where })) } -pub(super) fn check_derive_bitraversable( +pub fn check_derive_bitraversable( state: &mut CheckState, context: &CheckContext, class_file: FileId, diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index bafb6069..24041680 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -61,7 +61,7 @@ impl DerivedRigids { } } -pub(super) fn generate_variance_constraints( +pub fn generate_variance_constraints( state: &mut CheckState, context: &CheckContext, data_file: FileId, From 0b09efa7bc3343b1b558d1cccb46021a50ffaa95 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 20:41:05 +0800 Subject: [PATCH 350/386] Expand synonyms before type comparison --- compiler-core/checking2/src/core/unification.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 72ecc6f9..c4745b33 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -111,8 +111,8 @@ where P: SubtypePolicy, Q: ExternalQueries, { - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; + let t1 = normalise::normalise_expand(state, context, t1)?; + let t2 = normalise::normalise_expand(state, context, t2)?; if t1 == t2 { return Ok(true); @@ -235,8 +235,8 @@ pub fn unify( where Q: ExternalQueries, { - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; + let t1 = normalise::normalise_expand(state, context, t1)?; + let t2 = normalise::normalise_expand(state, context, t2)?; if t1 == t2 { return Ok(true); @@ -386,8 +386,8 @@ pub fn can_unify( where Q: ExternalQueries, { - let t1 = normalise::normalise(state, context, t1)?; - let t2 = normalise::normalise(state, context, t2)?; + let t1 = normalise::normalise_expand(state, context, t1)?; + let t2 = normalise::normalise_expand(state, context, t2)?; if t1 == t2 { return Ok(CanUnify::Equal); From 2795b1b8d8719f0a14980897163f8ddf0c079bd1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Fri, 6 Mar 2026 20:41:05 +0800 Subject: [PATCH 351/386] Fix kind arguments in synonym expansion --- compiler-core/checking2/src/core/normalise.rs | 79 ++++++++++++++----- .../checking2/src/source/type_items.rs | 18 +++-- .../149_synonym_oversaturation_kind/Main.purs | 19 +++++ .../149_synonym_oversaturation_kind/Main.snap | 26 ++++++ .../tests/checking2/generated.rs | 2 + 5 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.purs create mode 100644 tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.snap diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index f1bd42b3..716b8f3d 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -1,8 +1,9 @@ //! Implements normalisation algorithms for the core representation. +use std::iter; + use building_types::QueryResult; use itertools::Itertools; -use rustc_hash::FxHashMap; use crate::context::CheckContext; use crate::core::substitute::{NameToType, SubstituteName}; @@ -149,17 +150,19 @@ where Q: ExternalQueries, { let mut current = id; - let mut arguments = vec![]; + let mut applied_arguments = vec![]; + // Collect oversaturated arguments, in our example above, this + // would collect `[Int]` and `[Int, String]` into `arguments`. safe_loop! { current = normalise(state, context, current)?; match context.lookup_type(current) { Type::Application(function, argument) => { - arguments.push(KindOrType::Type(argument)); + applied_arguments.push(KindOrType::Type(argument)); current = function; } Type::KindApplication(function, argument) => { - arguments.push(KindOrType::Kind(argument)); + applied_arguments.push(KindOrType::Kind(argument)); current = function; } _ => break, @@ -171,44 +174,82 @@ where return Ok(id); }; - let synonym = context.lookup_synonym(synonym_id); - if synonym.saturation != Saturation::Full { + let synonym_application = context.lookup_synonym(synonym_id); + if synonym_application.saturation != Saturation::Full { return Ok(id); } - let (file_id, type_id) = synonym.reference; + let (file_id, type_id) = synonym_application.reference; let checked_synonym = toolkit::lookup_file_synonym(state, context, file_id, type_id)?; let Some(checked_synonym) = checked_synonym else { return Ok(id); }; - let mut parameter_arguments = synonym.arguments.iter().filter_map(|argument| match argument { - KindOrType::Type(argument) => Some(*argument), - KindOrType::Kind(_) => None, - }); + let mut bindings = NameToType::default(); + let mut kind = checked_synonym.kind; + let mut arguments = { + let synonym_arguments = synonym_application.arguments.iter().copied(); + let applied_arguments = applied_arguments.iter().copied().rev(); + iter::chain(synonym_arguments, applied_arguments) + }; + + // Create substitutions for kind arguments. For example, + // + // type T :: forall k. k -> Type + // type T (a :: k) = Proxy (a :: k) + // + // given an application such as, + // + // T @Type Int + // + // this loop produces the replacement, + // + // k := Type + // + // which is later substituted into the synonym body. Without this step, + // expansion would leave `k` rigid inside the synonym body causing + // unification errors downstream. + safe_loop! { + kind = normalise(state, context, kind)?; + + let Type::Forall(binder_id, inner) = context.lookup_type(kind) else { + break; + }; + + let Some(KindOrType::Kind(argument)) = arguments.next() else { + return Ok(id); + }; + + let binder = context.lookup_forall_binder(binder_id); + bindings.insert(binder.name, argument); + + kind = inner; + } - let mut bindings: NameToType = FxHashMap::default(); + // Create substitutions for type arguments. for parameter in &checked_synonym.parameters { - let Some(argument) = parameter_arguments.next() else { + let Some(KindOrType::Type(argument)) = arguments.next() else { return Ok(id); }; bindings.insert(parameter.name, argument); } - let mut expanded = if bindings.is_empty() { + // Apply the substitutions if there are any. + let mut substituted = if bindings.is_empty() { checked_synonym.synonym } else { SubstituteName::many(state, context, &bindings, checked_synonym.synonym)? }; - for argument in arguments.into_iter().rev() { - expanded = match argument { - KindOrType::Type(argument) => context.intern_application(expanded, argument), - KindOrType::Kind(argument) => context.intern_kind_application(expanded, argument), + // Reconstruct applications from remaining oversaturated arguments. + for argument in arguments { + substituted = match argument { + KindOrType::Type(argument) => context.intern_application(substituted, argument), + KindOrType::Kind(argument) => context.intern_kind_application(substituted, argument), }; } - Ok(expanded) + Ok(substituted) } pub fn normalise_expand( diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 7af3010f..8a264a9b 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -28,7 +28,6 @@ struct PendingDataType { } struct PendingSynonymType { - kind: TypeId, parameters: Vec, synonym: TypeId, } @@ -577,7 +576,7 @@ fn check_synonym_equation( where Q: ExternalQueries, { - let (parameters, kind, result) = if let Some(signature_id) = signature + let (parameters, result) = if let Some(signature_id) = signature && let Some(signature_kind) = state.checked.lookup_type(item_id) { check_synonym_equation_check(state, context, bindings, (signature_id, signature_kind))? @@ -592,7 +591,7 @@ where context.unknown("invalid synonym type") }; - scc.synonym.push((item_id, PendingSynonymType { kind, parameters, synonym })); + scc.synonym.push((item_id, PendingSynonymType { parameters, synonym })); Ok(()) } @@ -602,7 +601,7 @@ fn check_synonym_equation_check( context: &CheckContext, bindings: &[TypeVariableBinding], (signature_id, signature_kind): (lowering::TypeId, TypeId), -) -> QueryResult<(Vec, TypeId, TypeId)> +) -> QueryResult<(Vec, TypeId)> where Q: ExternalQueries, { @@ -613,7 +612,7 @@ where bindings, )?; let parameters = check_type_variable_bindings(state, context, bindings, &signature.arguments)?; - Ok((parameters, signature_kind, signature.result)) + Ok((parameters, signature.result)) } fn check_synonym_equation_infer( @@ -621,7 +620,7 @@ fn check_synonym_equation_infer( context: &CheckContext, item_id: TypeItemId, bindings: &[TypeVariableBinding], -) -> QueryResult<(Vec, TypeId, TypeId)> +) -> QueryResult<(Vec, TypeId)> where Q: ExternalQueries, { @@ -636,7 +635,7 @@ where state.checked.types.insert(item_id, inferred); } - Ok((bindings, inferred, result)) + Ok((bindings, result)) } fn finalise_synonym_replacements( @@ -647,7 +646,10 @@ fn finalise_synonym_replacements( where Q: ExternalQueries, { - for (item_id, PendingSynonymType { kind, parameters, synonym }) in mem::take(&mut scc.synonym) { + for (item_id, PendingSynonymType { parameters, synonym }) in mem::take(&mut scc.synonym) { + let Some(kind) = state.checked.lookup_type(item_id) else { + continue; + }; let synonym = zonk::zonk(state, context, synonym)?; let synonym = CheckedSynonym { kind, parameters, synonym }; state.checked.synonyms.insert(item_id, synonym); diff --git a/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.purs b/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.purs new file mode 100644 index 00000000..bccb121a --- /dev/null +++ b/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.purs @@ -0,0 +1,19 @@ +module Main where + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +type Test0 = Proxy + +test0 :: Test0 42 +test0 = Proxy + +type Test1 a = Proxy + +test1 :: Test1 Int 42 +test1 = Proxy + +type Test2 a b = Proxy + +test2 :: Test2 Int String 42 +test2 = Proxy diff --git a/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.snap b/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.snap new file mode 100644 index 00000000..33894cc9 --- /dev/null +++ b/tests-integration/fixtures/checking2/149_synonym_oversaturation_kind/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +test0 :: Test0 @Int 42 +test1 :: Test1 @Type @Int Int 42 +test2 :: Test2 @Type @Type @Int Int String 42 + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type +Test0 :: forall (t2 :: Type). (t2 :: Type) -> Type +Test1 :: forall (t5 :: Type) (t4 :: Type). (t5 :: Type) -> (t4 :: Type) -> Type +Test2 :: + forall (t10 :: Type) (t9 :: Type) (t8 :: Type). + (t10 :: Type) -> (t9 :: Type) -> (t8 :: Type) -> Type + +Synonyms +type Test0 = Proxy @(t2 :: Type) +type Test1 a = Proxy @(t4 :: Type) +type Test2 a b = Proxy @(t8 :: Type) + +Roles +Proxy = [Phantom] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index cbc35119..bb677e68 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -325,3 +325,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_147_synonym_oversaturation_equations_main() { run_test("147_synonym_oversaturation_equations", "Main"); } #[rustfmt::skip] #[test] fn test_148_class_member_prenex_main() { run_test("148_class_member_prenex", "Main"); } + +#[rustfmt::skip] #[test] fn test_149_synonym_oversaturation_kind_main() { run_test("149_synonym_oversaturation_kind", "Main"); } From c9954ffb77a2e0c0c6124d8ebfcbe7ecf7311d60 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 00:38:31 +0800 Subject: [PATCH 352/386] Update type-checker-tests to mention checking2 --- .agents/skills/type-checker-tests/SKILL.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.agents/skills/type-checker-tests/SKILL.md b/.agents/skills/type-checker-tests/SKILL.md index f288a0c5..84e5d461 100644 --- a/.agents/skills/type-checker-tests/SKILL.md +++ b/.agents/skills/type-checker-tests/SKILL.md @@ -1,12 +1,12 @@ --- name: type-checker-tests -description: Add integration tests for type checker inference and checking functions +description: Add integration tests for type checker inference and checking2 functions allowed-tools: Bash(mkdir:*) --- # Type Checker Integration Tests -Use the command reference at `reference/compiler-scripts.md` for test runner syntax, snapshot workflows, filters, and trace debugging. The category is `checking`. +Use the command reference at `reference/compiler-scripts.md` for test runner syntax, snapshot workflows, filters, and trace debugging. The category is `checking2`. **Language:** Fixtures use PureScript syntax, not Haskell. @@ -15,7 +15,7 @@ Use the command reference at `reference/compiler-scripts.md` for test runner syn ### 1. Create fixture directory ```bash -just t checking --create "descriptive name" +just t checking2 --create "descriptive name" ``` The CLI picks the next fixture number and creates the folder. @@ -24,7 +24,7 @@ Tests are auto-discovered by `build.rs`. ### 2. Write Main.purs -**Standard pattern** - pair typed (checking) and untyped (inference) variants: +**Standard pattern** - pair typed (checking2) and untyped (inference) variants: ```purescript module Main where @@ -45,16 +45,16 @@ test' [x] = x ### 3. Run and review ```bash -just t checking NNN MMM +just t checking2 NNN MMM ``` ### 4. Accept or reject snapshots ```bash -just t checking NNN --diff # Inspect a fixture diff -just t checking NNN --accept # Accept a specific fixture -just t checking NNN --reject # Reject a specific fixture -just t checking --accept --confirm # Accept all pending snapshots +just t checking2 NNN --diff # Inspect a fixture diff +just t checking2 NNN --accept # Accept a specific fixture +just t checking2 NNN --reject # Reject a specific fixture +just t checking2 --accept --confirm # Accept all pending snapshots ``` ## Multi-File Tests @@ -62,7 +62,7 @@ just t checking --accept --confirm # Accept all pending snapshots For imports, re-exports, or cross-module behavior: ``` -tests-integration/fixtures/checking/NNN_import_test/ +tests-integration/fixtures/checking2/NNN_import_test/ ├── Main.purs # Test file (snapshot generated) ├── Lib.purs # Supporting module └── Main.snap # Generated snapshot From 7bdf70722603aa88fbf5e26257b9077ffcf07f4a Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 00:42:41 +0800 Subject: [PATCH 353/386] Remove stale .gitkeep --- tests-integration/fixtures/checking2/.gitkeep | 1 - tests-integration/tests/checking2/generated.rs | 2 -- 2 files changed, 3 deletions(-) delete mode 100644 tests-integration/fixtures/checking2/.gitkeep diff --git a/tests-integration/fixtures/checking2/.gitkeep b/tests-integration/fixtures/checking2/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/tests-integration/fixtures/checking2/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index bb677e68..d10f1dca 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -28,8 +28,6 @@ fn run_test(folder: &str, file: &str) { settings.bind(|| insta::assert_snapshot!(file, report)); } -#[rustfmt::skip] #[test] fn test_gitkeep_main() { run_test("gitkeep", "Main"); } - #[rustfmt::skip] #[test] fn test_001_foreign_check_main() { run_test("001_foreign_check", "Main"); } #[rustfmt::skip] #[test] fn test_002_foreign_recursive_main() { run_test("002_foreign_recursive", "Main"); } From ec4cc15c67ca80cf1ddd6fb91dbf64d8c519eade Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 00:46:16 +0800 Subject: [PATCH 354/386] Add test coverage for generic deriving --- .../Main.purs | 7 +++ .../Main.snap | 52 +++++++++++++++++++ .../Main.purs | 5 ++ .../Main.snap | 20 +++++++ .../Data.Generic.Rep.purs | 6 +++ .../152_derive_generic_missing_rep/Main.purs | 7 +++ .../152_derive_generic_missing_rep/Main.snap | 26 ++++++++++ .../tests/checking2/generated.rs | 6 +++ 8 files changed, 129 insertions(+) create mode 100644 tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.purs create mode 100644 tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.snap create mode 100644 tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.snap create mode 100644 tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Data.Generic.Rep.purs create mode 100644 tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.purs create mode 100644 tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.snap diff --git a/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.purs b/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.purs new file mode 100644 index 00000000..d0b16d42 --- /dev/null +++ b/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Generic.Rep (class Generic) + +data Box = Box Int + +derive instance Generic Box diff --git a/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.snap new file mode 100644 index 00000000..0de4ec95 --- /dev/null +++ b/tests-integration/fixtures/checking2/150_derive_generic_insufficient_params/Main.snap @@ -0,0 +1,52 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: Int -> Box + +Types +Box :: Type + +Roles +Box = [] + +Errors +CheckError { + kind: DeriveInvalidArity { + class_file: Idx::(23), + class_id: Idx::(0), + expected: 2, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: DeriveInvalidArity { + class_file: Idx::(23), + class_id: Idx::(0), + expected: 2, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.purs b/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.purs new file mode 100644 index 00000000..4d347339 --- /dev/null +++ b/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Data.Generic.Rep (class Generic) + +derive instance Generic (Int -> Int) _ diff --git a/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.snap b/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.snap new file mode 100644 index 00000000..75c7ab0f --- /dev/null +++ b/tests-integration/fixtures/checking2/151_derive_generic_not_constructor/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: CannotDeriveForType { + type_message: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Data.Generic.Rep.purs b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Data.Generic.Rep.purs new file mode 100644 index 00000000..25cb321d --- /dev/null +++ b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Data.Generic.Rep.purs @@ -0,0 +1,6 @@ +-- | This shadows the test prelude to remove the representation types. +module Data.Generic.Rep where + +class Generic a rep | a -> rep where + to :: rep -> a + from :: a -> rep diff --git a/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.purs b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.purs new file mode 100644 index 00000000..9aceff83 --- /dev/null +++ b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Generic.Rep (class Generic) + +data Box = Box Int + +derive instance Generic Box _ diff --git a/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.snap b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.snap new file mode 100644 index 00000000..9c8b9ce0 --- /dev/null +++ b/tests-integration/fixtures/checking2/152_derive_generic_missing_rep/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: Int -> Box + +Types +Box :: Type + +Roles +Box = [] + +Errors +CheckError { + kind: CannotDeriveClass { + class_file: Idx::(39), + class_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index d10f1dca..c33dd194 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -325,3 +325,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_148_class_member_prenex_main() { run_test("148_class_member_prenex", "Main"); } #[rustfmt::skip] #[test] fn test_149_synonym_oversaturation_kind_main() { run_test("149_synonym_oversaturation_kind", "Main"); } + +#[rustfmt::skip] #[test] fn test_150_derive_generic_insufficient_params_main() { run_test("150_derive_generic_insufficient_params", "Main"); } + +#[rustfmt::skip] #[test] fn test_151_derive_generic_not_constructor_main() { run_test("151_derive_generic_not_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_152_derive_generic_missing_rep_main() { run_test("152_derive_generic_missing_rep", "Main"); } From 5fad84041f6d5a2a2c03142d0432f04c9cae08cc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 01:00:58 +0800 Subject: [PATCH 355/386] Add test coverage for do/ado blocks --- .../checking2/153_do_discard/Main.purs | 21 ++++ .../checking2/153_do_discard/Main.snap | 25 +++++ .../fixtures/checking2/154_do_bind/Main.purs | 20 ++++ .../fixtures/checking2/154_do_bind/Main.snap | 24 ++++ .../checking2/155_ado_discard/Main.purs | 23 ++++ .../checking2/155_ado_discard/Main.snap | 25 +++++ .../fixtures/checking2/156_ado_bind/Main.purs | 20 ++++ .../fixtures/checking2/156_ado_bind/Main.snap | 24 ++++ .../checking2/157_do_polymorphic/Main.purs | 19 ++++ .../checking2/157_do_polymorphic/Main.snap | 26 +++++ .../checking2/158_ado_polymorphic/Main.purs | 19 ++++ .../checking2/158_ado_polymorphic/Main.snap | 26 +++++ .../checking2/159_do_empty_block/Main.purs | 9 ++ .../checking2/159_do_empty_block/Main.snap | 44 ++++++++ .../checking2/160_ado_empty_block/Main.purs | 12 ++ .../checking2/160_ado_empty_block/Main.snap | 45 ++++++++ .../161_do_ado_constrained/Main.purs | 51 +++++++++ .../161_do_ado_constrained/Main.snap | 103 ++++++++++++++++++ .../162_do_let_premature_solve/Main.purs | 28 +++++ .../162_do_let_premature_solve/Main.snap | 67 ++++++++++++ .../163_do_let_annotation_solve/Main.purs | 20 ++++ .../163_do_let_annotation_solve/Main.snap | 47 ++++++++ .../164_ado_let_premature_solve/Main.purs | 28 +++++ .../164_ado_let_premature_solve/Main.snap | 69 ++++++++++++ .../165_ado_let_annotation_solve/Main.purs | 20 ++++ .../165_ado_let_annotation_solve/Main.snap | 49 +++++++++ .../checking2/166_do_bind_only/Main.purs | 14 +++ .../checking2/166_do_bind_only/Main.snap | 12 ++ .../checking2/167_do_final_bind/Main.purs | 12 ++ .../checking2/167_do_final_bind/Main.snap | 40 +++++++ .../checking2/168_do_final_let/Main.purs | 10 ++ .../checking2/168_do_final_let/Main.snap | 65 +++++++++++ .../tests/checking2/generated.rs | 34 ++++++ 33 files changed, 1051 insertions(+) create mode 100644 tests-integration/fixtures/checking2/153_do_discard/Main.purs create mode 100644 tests-integration/fixtures/checking2/153_do_discard/Main.snap create mode 100644 tests-integration/fixtures/checking2/154_do_bind/Main.purs create mode 100644 tests-integration/fixtures/checking2/154_do_bind/Main.snap create mode 100644 tests-integration/fixtures/checking2/155_ado_discard/Main.purs create mode 100644 tests-integration/fixtures/checking2/155_ado_discard/Main.snap create mode 100644 tests-integration/fixtures/checking2/156_ado_bind/Main.purs create mode 100644 tests-integration/fixtures/checking2/156_ado_bind/Main.snap create mode 100644 tests-integration/fixtures/checking2/157_do_polymorphic/Main.purs create mode 100644 tests-integration/fixtures/checking2/157_do_polymorphic/Main.snap create mode 100644 tests-integration/fixtures/checking2/158_ado_polymorphic/Main.purs create mode 100644 tests-integration/fixtures/checking2/158_ado_polymorphic/Main.snap create mode 100644 tests-integration/fixtures/checking2/159_do_empty_block/Main.purs create mode 100644 tests-integration/fixtures/checking2/159_do_empty_block/Main.snap create mode 100644 tests-integration/fixtures/checking2/160_ado_empty_block/Main.purs create mode 100644 tests-integration/fixtures/checking2/160_ado_empty_block/Main.snap create mode 100644 tests-integration/fixtures/checking2/161_do_ado_constrained/Main.purs create mode 100644 tests-integration/fixtures/checking2/161_do_ado_constrained/Main.snap create mode 100644 tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.purs create mode 100644 tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.snap create mode 100644 tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.purs create mode 100644 tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.snap create mode 100644 tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.purs create mode 100644 tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.snap create mode 100644 tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.purs create mode 100644 tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.snap create mode 100644 tests-integration/fixtures/checking2/166_do_bind_only/Main.purs create mode 100644 tests-integration/fixtures/checking2/166_do_bind_only/Main.snap create mode 100644 tests-integration/fixtures/checking2/167_do_final_bind/Main.purs create mode 100644 tests-integration/fixtures/checking2/167_do_final_bind/Main.snap create mode 100644 tests-integration/fixtures/checking2/168_do_final_let/Main.purs create mode 100644 tests-integration/fixtures/checking2/168_do_final_let/Main.snap diff --git a/tests-integration/fixtures/checking2/153_do_discard/Main.purs b/tests-integration/fixtures/checking2/153_do_discard/Main.purs new file mode 100644 index 00000000..4bc8e403 --- /dev/null +++ b/tests-integration/fixtures/checking2/153_do_discard/Main.purs @@ -0,0 +1,21 @@ +module Main where + +foreign import data Effect :: Type -> Type + +data Unit = Unit + +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +unit :: Unit +unit = Unit + +test :: Effect Unit +test = do + pure 1 + pure unit + +test' = do + pure 1 + pure unit diff --git a/tests-integration/fixtures/checking2/153_do_discard/Main.snap b/tests-integration/fixtures/checking2/153_do_discard/Main.snap new file mode 100644 index 00000000..239b5b9b --- /dev/null +++ b/tests-integration/fixtures/checking2/153_do_discard/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +unit :: Unit +test :: Effect Unit +test' :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] diff --git a/tests-integration/fixtures/checking2/154_do_bind/Main.purs b/tests-integration/fixtures/checking2/154_do_bind/Main.purs new file mode 100644 index 00000000..b95434c6 --- /dev/null +++ b/tests-integration/fixtures/checking2/154_do_bind/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type + +data Tuple a b = Tuple a b + +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +test :: Effect (Tuple Int Int) +test = do + x <- pure 1 + y <- pure 2 + pure (Tuple x y) + +test' = do + x <- pure 1 + y <- pure 2 + pure (Tuple x y) diff --git a/tests-integration/fixtures/checking2/154_do_bind/Main.snap b/tests-integration/fixtures/checking2/154_do_bind/Main.snap new file mode 100644 index 00000000..42f16f93 --- /dev/null +++ b/tests-integration/fixtures/checking2/154_do_bind/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +test :: Effect (Tuple Int Int) +test' :: Effect (Tuple Int Int) + +Types +Effect :: Type -> Type +Tuple :: Type -> Type -> Type + +Roles +Effect = [Nominal] +Tuple = [Representational, Representational] diff --git a/tests-integration/fixtures/checking2/155_ado_discard/Main.purs b/tests-integration/fixtures/checking2/155_ado_discard/Main.purs new file mode 100644 index 00000000..7e74a291 --- /dev/null +++ b/tests-integration/fixtures/checking2/155_ado_discard/Main.purs @@ -0,0 +1,23 @@ +module Main where + +foreign import data Effect :: Type -> Type + +data Unit = Unit + +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +unit :: Unit +unit = Unit + +test :: Effect Unit +test = ado + pure 1 + pure unit + in unit + +test' = ado + pure 1 + pure unit + in unit diff --git a/tests-integration/fixtures/checking2/155_ado_discard/Main.snap b/tests-integration/fixtures/checking2/155_ado_discard/Main.snap new file mode 100644 index 00000000..9acf8058 --- /dev/null +++ b/tests-integration/fixtures/checking2/155_ado_discard/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +unit :: Unit +test :: Effect Unit +test' :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] diff --git a/tests-integration/fixtures/checking2/156_ado_bind/Main.purs b/tests-integration/fixtures/checking2/156_ado_bind/Main.purs new file mode 100644 index 00000000..babb2031 --- /dev/null +++ b/tests-integration/fixtures/checking2/156_ado_bind/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type + +data Tuple a b = Tuple a b + +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +test :: Effect (Tuple Int Int) +test = ado + x <- pure 1 + y <- pure 2 + in Tuple x y + +test' = ado + x <- pure 1 + y <- pure 2 + in Tuple x y diff --git a/tests-integration/fixtures/checking2/156_ado_bind/Main.snap b/tests-integration/fixtures/checking2/156_ado_bind/Main.snap new file mode 100644 index 00000000..893d3c0a --- /dev/null +++ b/tests-integration/fixtures/checking2/156_ado_bind/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +test :: Effect (Tuple Int Int) +test' :: Effect (Tuple Int Int) + +Types +Effect :: Type -> Type +Tuple :: Type -> Type -> Type + +Roles +Effect = [Nominal] +Tuple = [Representational, Representational] diff --git a/tests-integration/fixtures/checking2/157_do_polymorphic/Main.purs b/tests-integration/fixtures/checking2/157_do_polymorphic/Main.purs new file mode 100644 index 00000000..6c9de827 --- /dev/null +++ b/tests-integration/fixtures/checking2/157_do_polymorphic/Main.purs @@ -0,0 +1,19 @@ +module Main where + +data Tuple a b = Tuple a b + +foreign import bind :: forall m a b. m a -> (a -> m b) -> m b +foreign import discard :: forall m a b. m a -> (a -> m b) -> m b + +foreign import pure :: forall m a. a -> m a + +test :: forall m. m (Tuple Int String) +test = do + x <- pure 1 + y <- pure "hello" + pure (Tuple x y) + +test' = do + x <- pure 1 + y <- pure "hello" + pure (Tuple x y) diff --git a/tests-integration/fixtures/checking2/157_do_polymorphic/Main.snap b/tests-integration/fixtures/checking2/157_do_polymorphic/Main.snap new file mode 100644 index 00000000..201f0736 --- /dev/null +++ b/tests-integration/fixtures/checking2/157_do_polymorphic/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +bind :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +discard :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +pure :: forall (m :: Type -> Type) (a :: Type). (a :: Type) -> (m :: Type -> Type) (a :: Type) +test :: forall (m :: Type -> Type). (m :: Type -> Type) (Tuple Int String) +test' :: forall (t11 :: Type -> Type). (t11 :: Type -> Type) (Tuple Int String) + +Types +Tuple :: Type -> Type -> Type + +Roles +Tuple = [Representational, Representational] diff --git a/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.purs b/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.purs new file mode 100644 index 00000000..572e455b --- /dev/null +++ b/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.purs @@ -0,0 +1,19 @@ +module Main where + +data Tuple a b = Tuple a b + +foreign import map :: forall f a b. (a -> b) -> f a -> f b +foreign import apply :: forall f a b. f (a -> b) -> f a -> f b + +foreign import pure :: forall f a. a -> f a + +test :: forall f. f (Tuple Int String) +test = ado + x <- pure 1 + y <- pure "hello" + in Tuple x y + +test' = ado + x <- pure 1 + y <- pure "hello" + in Tuple x y diff --git a/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.snap b/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.snap new file mode 100644 index 00000000..dbeaaa82 --- /dev/null +++ b/tests-integration/fixtures/checking2/158_ado_polymorphic/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +apply :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + (f :: Type -> Type) ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +pure :: forall (f :: Type -> Type) (a :: Type). (a :: Type) -> (f :: Type -> Type) (a :: Type) +test :: forall (f :: Type -> Type). (f :: Type -> Type) (Tuple Int String) +test' :: forall (t11 :: Type -> Type). (t11 :: Type -> Type) (Tuple Int String) + +Types +Tuple :: Type -> Type -> Type + +Roles +Tuple = [Representational, Representational] diff --git a/tests-integration/fixtures/checking2/159_do_empty_block/Main.purs b/tests-integration/fixtures/checking2/159_do_empty_block/Main.purs new file mode 100644 index 00000000..ed4e84ff --- /dev/null +++ b/tests-integration/fixtures/checking2/159_do_empty_block/Main.purs @@ -0,0 +1,9 @@ +module Main where + +foreign import data Effect :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a b. Effect a -> (a -> Effect b) -> Effect b + +test = do diff --git a/tests-integration/fixtures/checking2/159_do_empty_block/Main.snap b/tests-integration/fixtures/checking2/159_do_empty_block/Main.snap new file mode 100644 index 00000000..b2727c05 --- /dev/null +++ b/tests-integration/fixtures/checking2/159_do_empty_block/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +test :: ?[empty do block] + +Types +Effect :: Type -> Type + +Roles +Effect = [Nominal] + +Errors +CheckError { + kind: EmptyDoBlock, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + InferringExpression( + AstId(56), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(9), + t2: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/160_ado_empty_block/Main.purs b/tests-integration/fixtures/checking2/160_ado_empty_block/Main.purs new file mode 100644 index 00000000..2dff67e6 --- /dev/null +++ b/tests-integration/fixtures/checking2/160_ado_empty_block/Main.purs @@ -0,0 +1,12 @@ +module Main where + +foreign import data Effect :: Type -> Type + +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +test1 = ado + +test2 = ado + in 123 diff --git a/tests-integration/fixtures/checking2/160_ado_empty_block/Main.snap b/tests-integration/fixtures/checking2/160_ado_empty_block/Main.snap new file mode 100644 index 00000000..75cbaaf1 --- /dev/null +++ b/tests-integration/fixtures/checking2/160_ado_empty_block/Main.snap @@ -0,0 +1,45 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +test1 :: ?[empty ado block] +test2 :: Effect Int + +Types +Effect :: Type -> Type + +Roles +Effect = [Nominal] + +Errors +CheckError { + kind: EmptyAdoBlock, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + InferringExpression( + AstId(54), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.purs b/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.purs new file mode 100644 index 00000000..a2656963 --- /dev/null +++ b/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.purs @@ -0,0 +1,51 @@ +module Main where + +data Tuple a b = Tuple a b + +class Functor f where + map :: forall a b. (a -> b) -> f a -> f b + +class Functor f <= Apply f where + apply :: forall a b. f (a -> b) -> f a -> f b + +class Apply f <= Applicative f where + pure :: forall a. a -> f a + +class Applicative m <= Discard m where + discard :: forall a b. m a -> (a -> m b) -> m b + +class Applicative m <= Bind m where + bind :: forall a b. m a -> (a -> m b) -> m b + +class Bind m <= Monad m + +testDo :: forall m. Monad m => m (Tuple Int String) +testDo = do + x <- pure 1 + y <- pure "hello" + pure (Tuple x y) + +testDo' = do + x <- pure 1 + y <- pure "hello" + pure (Tuple x y) + +testAdo :: forall f. Applicative f => f (Tuple Int String) +testAdo = ado + x <- pure 1 + y <- pure "hello" + in Tuple x y + +testAdo' = ado + x <- pure 1 + y <- pure "hello" + in Tuple x y + +testDoDiscard :: forall m. Monad m => m Int +testDoDiscard = do + pure "ignored" + pure 42 + +testDoDiscard' = do + pure "ignored" + pure 42 diff --git a/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.snap b/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.snap new file mode 100644 index 00000000..eb020d53 --- /dev/null +++ b/tests-integration/fixtures/checking2/161_do_ado_constrained/Main.snap @@ -0,0 +1,103 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +apply :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Apply (f :: Type -> Type) => + (f :: Type -> Type) ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +pure :: + forall (f :: Type -> Type) (a :: Type). + Applicative (f :: Type -> Type) => (a :: Type) -> (f :: Type -> Type) (a :: Type) +discard :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Discard (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +bind :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Bind (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +testDo :: + forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) (Tuple Int String) +testDo' :: + forall (t18 :: Type -> Type). + Bind (t18 :: Type -> Type) => (t18 :: Type -> Type) (Tuple Int String) +testAdo :: + forall (f :: Type -> Type). + Applicative (f :: Type -> Type) => (f :: Type -> Type) (Tuple Int String) +testAdo' :: + forall (t20 :: Type -> Type). + Applicative (t20 :: Type -> Type) => (t20 :: Type -> Type) (Tuple Int String) +testDoDiscard :: forall (m :: Type -> Type). Monad (m :: Type -> Type) => (m :: Type -> Type) Int +testDoDiscard' :: + forall (t22 :: Type -> Type). Discard (t22 :: Type -> Type) => (t22 :: Type -> Type) Int + +Types +Tuple :: Type -> Type -> Type +Functor :: (Type -> Type) -> Constraint +Apply :: (Type -> Type) -> Constraint +Applicative :: (Type -> Type) -> Constraint +Discard :: (Type -> Type) -> Constraint +Bind :: (Type -> Type) -> Constraint +Monad :: (Type -> Type) -> Constraint + +Classes +class forall (f :: Type -> Type). Functor (f :: Type -> Type) + map :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Functor (f :: Type -> Type) => + ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +class forall (f :: Type -> Type). Functor (f :: Type -> Type) <= Apply (f :: Type -> Type) + apply :: + forall (f :: Type -> Type) (a :: Type) (b :: Type). + Apply (f :: Type -> Type) => + (f :: Type -> Type) ((a :: Type) -> (b :: Type)) -> + (f :: Type -> Type) (a :: Type) -> + (f :: Type -> Type) (b :: Type) +class forall (f :: Type -> Type). Apply (f :: Type -> Type) <= Applicative (f :: Type -> Type) + pure :: + forall (f :: Type -> Type) (a :: Type). + Applicative (f :: Type -> Type) => (a :: Type) -> (f :: Type -> Type) (a :: Type) +class forall (m :: Type -> Type). Applicative (m :: Type -> Type) <= Discard (m :: Type -> Type) + discard :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Discard (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +class forall (m :: Type -> Type). Applicative (m :: Type -> Type) <= Bind (m :: Type -> Type) + bind :: + forall (m :: Type -> Type) (a :: Type) (b :: Type). + Bind (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> + ((a :: Type) -> (m :: Type -> Type) (b :: Type)) -> + (m :: Type -> Type) (b :: Type) +class forall (m :: Type -> Type). Bind (m :: Type -> Type) <= Monad (m :: Type -> Type) + +Roles +Tuple = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(11), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.purs b/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.purs new file mode 100644 index 00000000..2aa41d84 --- /dev/null +++ b/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.purs @@ -0,0 +1,28 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a. Effect a -> Effect Unit -> Effect Unit + +class Semiring a where + add :: a -> a -> a + +instance Semiring Int where + add = addImpl + +foreign import addImpl :: Int -> Int -> Int + +infixl 6 add as + + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = do + a <- thing1 + let f = a + 123 + pure unit diff --git a/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.snap b/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.snap new file mode 100644 index 00000000..e0881335 --- /dev/null +++ b/tests-integration/fixtures/checking2/162_do_let_premature_solve/Main.snap @@ -0,0 +1,67 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: forall (a :: Type). Effect (a :: Type) -> Effect Unit -> Effect Unit +add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +addImpl :: Int -> Int -> Int ++ :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type +Semiring :: Type -> Constraint + +Classes +class forall (a :: Type). Semiring (a :: Type) + add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) + +Instances +instance Semiring Int + +Roles +Effect = [Nominal] +Unit = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(9), + ), + CheckingExpression( + AstId(97), + ), + CheckingDoLet( + AstId(102), + ), + CheckingLetName( + Idx::(0), + ), + InferringExpression( + AstId(107), + ), + CheckingExpression( + AstId(111), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.purs b/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.purs new file mode 100644 index 00000000..9ab3a657 --- /dev/null +++ b/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import bind :: forall a b. Effect a -> (a -> Effect b) -> Effect b +foreign import discard :: forall a. Effect a -> Effect Unit -> Effect Unit + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = do + a <- thing1 + let + f :: Int + f = a + pure unit diff --git a/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.snap new file mode 100644 index 00000000..d92ebba5 --- /dev/null +++ b/tests-integration/fixtures/checking2/163_do_let_annotation_solve/Main.snap @@ -0,0 +1,47 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +bind :: + forall (a :: Type) (b :: Type). + Effect (a :: Type) -> ((a :: Type) -> Effect (b :: Type)) -> Effect (b :: Type) +discard :: forall (a :: Type). Effect (a :: Type) -> Effect Unit -> Effect Unit +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + CheckingExpression( + AstId(71), + ), + CheckingDoLet( + AstId(76), + ), + CheckingLetName( + Idx::(0), + ), + CheckingExpression( + AstId(83), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.purs b/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.purs new file mode 100644 index 00000000..77f9722c --- /dev/null +++ b/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.purs @@ -0,0 +1,28 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +class Semiring a where + add :: a -> a -> a + +instance Semiring Int where + add = addImpl + +foreign import addImpl :: Int -> Int -> Int + +infixl 6 add as + + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = ado + a <- thing1 + let f = a + 123 + in unit diff --git a/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.snap b/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.snap new file mode 100644 index 00000000..0ec553bb --- /dev/null +++ b/tests-integration/fixtures/checking2/164_ado_let_premature_solve/Main.snap @@ -0,0 +1,69 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +addImpl :: Int -> Int -> Int ++ :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type +Semiring :: Type -> Constraint + +Classes +class forall (a :: Type). Semiring (a :: Type) + add :: forall (a :: Type). Semiring (a :: Type) => (a :: Type) -> (a :: Type) -> (a :: Type) + +Instances +instance Semiring Int + +Roles +Effect = [Nominal] +Unit = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(9), + ), + CheckingExpression( + AstId(99), + ), + CheckingAdoLet( + AstId(104), + ), + CheckingLetName( + Idx::(0), + ), + InferringExpression( + AstId(109), + ), + CheckingExpression( + AstId(113), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(12), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.purs b/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.purs new file mode 100644 index 00000000..0728bc90 --- /dev/null +++ b/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.purs @@ -0,0 +1,20 @@ +module Main where + +foreign import data Effect :: Type -> Type +foreign import data Unit :: Type + +foreign import unit :: Unit +foreign import pure :: forall a. a -> Effect a +foreign import map :: forall a b. (a -> b) -> Effect a -> Effect b +foreign import apply :: forall a b. Effect (a -> b) -> Effect a -> Effect b + +thing1 :: Effect String +thing1 = pure "hello" + +test :: Effect Unit +test = ado + a <- thing1 + let + f :: Int + f = a + in unit diff --git a/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.snap b/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.snap new file mode 100644 index 00000000..858c0f20 --- /dev/null +++ b/tests-integration/fixtures/checking2/165_ado_let_annotation_solve/Main.snap @@ -0,0 +1,49 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unit :: Unit +pure :: forall (a :: Type). (a :: Type) -> Effect (a :: Type) +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +apply :: + forall (a :: Type) (b :: Type). + Effect ((a :: Type) -> (b :: Type)) -> Effect (a :: Type) -> Effect (b :: Type) +thing1 :: Effect String +test :: Effect Unit + +Types +Effect :: Type -> Type +Unit :: Type + +Roles +Effect = [Nominal] +Unit = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + CheckingExpression( + AstId(73), + ), + CheckingAdoLet( + AstId(78), + ), + CheckingLetName( + Idx::(0), + ), + CheckingExpression( + AstId(85), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/166_do_bind_only/Main.purs b/tests-integration/fixtures/checking2/166_do_bind_only/Main.purs new file mode 100644 index 00000000..2e40cc8e --- /dev/null +++ b/tests-integration/fixtures/checking2/166_do_bind_only/Main.purs @@ -0,0 +1,14 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind) +import Effect (Effect) + +test :: Effect Int +test = do + x <- pure 1 + pure x + +test' = do + x <- pure 1 + pure x diff --git a/tests-integration/fixtures/checking2/166_do_bind_only/Main.snap b/tests-integration/fixtures/checking2/166_do_bind_only/Main.snap new file mode 100644 index 00000000..3b5b70ae --- /dev/null +++ b/tests-integration/fixtures/checking2/166_do_bind_only/Main.snap @@ -0,0 +1,12 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: + forall (t0 :: Type -> Type). + Bind (t0 :: Type -> Type) => Applicative (t0 :: Type -> Type) => (t0 :: Type -> Type) Int + +Types diff --git a/tests-integration/fixtures/checking2/167_do_final_bind/Main.purs b/tests-integration/fixtures/checking2/167_do_final_bind/Main.purs new file mode 100644 index 00000000..9c3019d6 --- /dev/null +++ b/tests-integration/fixtures/checking2/167_do_final_bind/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Control.Applicative (pure) +import Control.Bind (bind) +import Effect (Effect) + +test :: Effect Int +test = do + x <- pure 1 + +test' = do + x <- pure 1 diff --git a/tests-integration/fixtures/checking2/167_do_final_bind/Main.snap b/tests-integration/fixtures/checking2/167_do_final_bind/Main.snap new file mode 100644 index 00000000..78333dd2 --- /dev/null +++ b/tests-integration/fixtures/checking2/167_do_final_bind/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: forall (t0 :: Type -> Type). Applicative (t0 :: Type -> Type) => (t0 :: Type -> Type) Int + +Types + +Errors +CheckError { + kind: InvalidFinalBind, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(27), + ), + InferringDoBind( + AstId(29), + ), + ], +} +CheckError { + kind: InvalidFinalBind, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + InferringExpression( + AstId(38), + ), + InferringDoBind( + AstId(40), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/168_do_final_let/Main.purs b/tests-integration/fixtures/checking2/168_do_final_let/Main.purs new file mode 100644 index 00000000..bfcd874e --- /dev/null +++ b/tests-integration/fixtures/checking2/168_do_final_let/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Effect (Effect) + +test :: Effect Int +test = do + let x = 1 + +test' = do + let x = 1 diff --git a/tests-integration/fixtures/checking2/168_do_final_let/Main.snap b/tests-integration/fixtures/checking2/168_do_final_let/Main.snap new file mode 100644 index 00000000..a18e85e1 --- /dev/null +++ b/tests-integration/fixtures/checking2/168_do_final_let/Main.snap @@ -0,0 +1,65 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: Effect Int +test' :: ?[invalid final let] + +Types + +Errors +CheckError { + kind: InvalidFinalLet, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(17), + ), + CheckingDoLet( + AstId(19), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(11), + t2: Id(12), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(17), + ), + ], +} +CheckError { + kind: InvalidFinalLet, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + InferringExpression( + AstId(28), + ), + CheckingDoLet( + AstId(30), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(13), + t2: Id(14), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index c33dd194..21f64e2b 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -331,3 +331,37 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_151_derive_generic_not_constructor_main() { run_test("151_derive_generic_not_constructor", "Main"); } #[rustfmt::skip] #[test] fn test_152_derive_generic_missing_rep_main() { run_test("152_derive_generic_missing_rep", "Main"); } + +#[rustfmt::skip] #[test] fn test_153_do_discard_main() { run_test("153_do_discard", "Main"); } + +#[rustfmt::skip] #[test] fn test_154_do_bind_main() { run_test("154_do_bind", "Main"); } + +#[rustfmt::skip] #[test] fn test_155_ado_discard_main() { run_test("155_ado_discard", "Main"); } + +#[rustfmt::skip] #[test] fn test_156_ado_bind_main() { run_test("156_ado_bind", "Main"); } + +#[rustfmt::skip] #[test] fn test_157_do_polymorphic_main() { run_test("157_do_polymorphic", "Main"); } + +#[rustfmt::skip] #[test] fn test_158_ado_polymorphic_main() { run_test("158_ado_polymorphic", "Main"); } + +#[rustfmt::skip] #[test] fn test_159_do_empty_block_main() { run_test("159_do_empty_block", "Main"); } + +#[rustfmt::skip] #[test] fn test_160_ado_empty_block_main() { run_test("160_ado_empty_block", "Main"); } + +#[rustfmt::skip] #[test] fn test_161_do_ado_constrained_main() { run_test("161_do_ado_constrained", "Main"); } + +#[rustfmt::skip] #[test] fn test_162_do_let_premature_solve_main() { run_test("162_do_let_premature_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_163_do_let_annotation_solve_main() { run_test("163_do_let_annotation_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_164_ado_let_premature_solve_main() { run_test("164_ado_let_premature_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_165_ado_let_annotation_solve_main() { run_test("165_ado_let_annotation_solve", "Main"); } + +#[rustfmt::skip] #[test] fn test_166_do_bind_only_main() { run_test("166_do_bind_only", "Main"); } + +#[rustfmt::skip] #[test] fn test_167_do_final_bind_main() { run_test("167_do_final_bind", "Main"); } + +#[rustfmt::skip] #[test] fn test_168_do_final_let_main() { run_test("168_do_final_let", "Main"); } + +#[rustfmt::skip] #[test] fn test_169_exhaustive_multiple_main() { run_test("169_exhaustive_multiple", "Main"); } From 08ed53b0161ec54047e07d627db60e48cc5a3c96 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 01:09:55 +0800 Subject: [PATCH 356/386] Add test coverage for exhaustiveness --- .../169_exhaustive_multiple/Main.purs | 29 ++++++ .../169_exhaustive_multiple/Main.snap | 85 +++++++++++++++++ .../checking2/170_exhaustive_tuple/Main.purs | 31 +++++++ .../checking2/170_exhaustive_tuple/Main.snap | 92 +++++++++++++++++++ .../Main.purs | 25 +++++ .../Main.snap | 72 +++++++++++++++ .../172_exhaustive_equation_guarded/Main.purs | 9 ++ .../172_exhaustive_equation_guarded/Main.snap | 24 +++++ .../173_exhaustive_let_equation/Main.purs | 17 ++++ .../173_exhaustive_let_equation/Main.snap | 16 ++++ .../Main.purs | 7 ++ .../Main.snap | 31 +++++++ .../Main.purs | 19 ++++ .../Main.snap | 57 ++++++++++++ .../checking2/176_array_exhaustive/Main.purs | 15 +++ .../checking2/176_array_exhaustive/Main.snap | 37 ++++++++ .../Main.purs | 27 ++++++ .../Main.snap | 13 +++ .../tests/checking2/generated.rs | 16 ++++ 19 files changed, 622 insertions(+) create mode 100644 tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.purs create mode 100644 tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.snap create mode 100644 tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.purs create mode 100644 tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.snap create mode 100644 tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.purs create mode 100644 tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.snap create mode 100644 tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.purs create mode 100644 tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.snap create mode 100644 tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.purs create mode 100644 tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap create mode 100644 tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.purs create mode 100644 tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.snap create mode 100644 tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.snap create mode 100644 tests-integration/fixtures/checking2/176_array_exhaustive/Main.purs create mode 100644 tests-integration/fixtures/checking2/176_array_exhaustive/Main.snap create mode 100644 tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.purs create mode 100644 tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.snap diff --git a/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.purs b/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.purs new file mode 100644 index 00000000..bfe45529 --- /dev/null +++ b/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.purs @@ -0,0 +1,29 @@ +module Main where + +data Maybe a = Just a | Nothing + +complete = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete1 = case _, _ of + Just _, Nothing -> 2 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete2 = case _, _ of + Just _, Just _ -> 1 + Nothing, Just _ -> 3 + Nothing, Nothing -> 4 + +incomplete3 = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Nothing -> 4 + +incomplete4 = case _, _ of + Just _, Just _ -> 1 + Just _, Nothing -> 2 + Nothing, Just _ -> 3 diff --git a/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.snap b/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.snap new file mode 100644 index 00000000..c87c579c --- /dev/null +++ b/tests-integration/fixtures/checking2/169_exhaustive_multiple/Main.snap @@ -0,0 +1,85 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +complete :: forall (t2 :: Type) (t1 :: Type). Maybe (t2 :: Type) -> Maybe (t1 :: Type) -> Int +incomplete1 :: + forall (t4 :: Type) (t3 :: Type). Partial => Maybe (t4 :: Type) -> Maybe (t3 :: Type) -> Int +incomplete2 :: + forall (t6 :: Type) (t5 :: Type). Partial => Maybe (t6 :: Type) -> Maybe (t5 :: Type) -> Int +incomplete3 :: + forall (t8 :: Type) (t7 :: Type). Partial => Maybe (t8 :: Type) -> Maybe (t7 :: Type) -> Int +incomplete4 :: + forall (t10 :: Type) (t9 :: Type). Partial => Maybe (t10 :: Type) -> Maybe (t9 :: Type) -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + InferringExpression( + AstId(53), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(8), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + InferringExpression( + AstId(84), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(9), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + InferringExpression( + AstId(116), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(10), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + InferringExpression( + AstId(148), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.purs b/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.purs new file mode 100644 index 00000000..9e80fa60 --- /dev/null +++ b/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.purs @@ -0,0 +1,31 @@ +module Main where + +data Tuple a b = Tuple a b + +data Maybe a = Just a | Nothing + +complete = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete1 = case _ of + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete2 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple Nothing (Just _) -> 3 + Tuple Nothing Nothing -> 4 + +incomplete3 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing Nothing -> 4 + +incomplete4 = case _ of + Tuple (Just _) (Just _) -> 1 + Tuple (Just _) Nothing -> 2 + Tuple Nothing (Just _) -> 3 diff --git a/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.snap b/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.snap new file mode 100644 index 00000000..17629e0c --- /dev/null +++ b/tests-integration/fixtures/checking2/170_exhaustive_tuple/Main.snap @@ -0,0 +1,92 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Tuple :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> Tuple (a :: Type) (b :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +complete :: forall (t4 :: Type) (t3 :: Type). Tuple (Maybe (t4 :: Type)) (Maybe (t3 :: Type)) -> Int +incomplete1 :: + forall (t6 :: Type) (t5 :: Type). + Partial => Tuple (Maybe (t6 :: Type)) (Maybe (t5 :: Type)) -> Int +incomplete2 :: + forall (t8 :: Type) (t7 :: Type). + Partial => Tuple (Maybe (t8 :: Type)) (Maybe (t7 :: Type)) -> Int +incomplete3 :: + forall (t10 :: Type) (t9 :: Type). + Partial => Tuple (Maybe (t10 :: Type)) (Maybe (t9 :: Type)) -> Int +incomplete4 :: + forall (t12 :: Type) (t11 :: Type). + Partial => Tuple (Maybe (t12 :: Type)) (Maybe (t11 :: Type)) -> Int + +Types +Tuple :: Type -> Type -> Type +Maybe :: Type -> Type + +Roles +Tuple = [Representational, Representational] +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(8), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + InferringExpression( + AstId(66), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(9), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + InferringExpression( + AstId(101), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(10), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + InferringExpression( + AstId(138), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(11), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(7), + ), + InferringExpression( + AstId(175), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.purs b/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.purs new file mode 100644 index 00000000..943cbf1d --- /dev/null +++ b/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.purs @@ -0,0 +1,25 @@ +module Main where + +data Unit = Unit + +unit :: Unit -> Int +unit Unit = 1 +unit _ = 2 +unit Unit = 3 + +data YesNo = Yes | No + +yes :: YesNo -> Int +yes Yes = 1 +yes _ = 2 +yes Yes = 3 + +no :: YesNo -> Int +no Yes = 1 +no _ = 2 +no No = 3 + +yesNo :: YesNo -> Int +yesNo Yes = 1 +yesNo No = 2 +yesNo _ = 3 diff --git a/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.snap b/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.snap new file mode 100644 index 00000000..5922962d --- /dev/null +++ b/tests-integration/fixtures/checking2/171_exhaustive_equation_redundant/Main.snap @@ -0,0 +1,72 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Unit :: Unit +unit :: Unit -> Int +Yes :: YesNo +No :: YesNo +yes :: YesNo -> Int +no :: YesNo -> Int +yesNo :: YesNo -> Int + +Types +Unit :: Type +YesNo :: Type + +Roles +Unit = [] +YesNo = [] + +Errors +CheckError { + kind: RedundantPatterns { + patterns: [ + "_", + "Unit", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: RedundantPatterns { + patterns: [ + "Yes", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: RedundantPatterns { + patterns: [ + "No", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} +CheckError { + kind: RedundantPatterns { + patterns: [ + "_", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.purs b/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.purs new file mode 100644 index 00000000..0bfc36ba --- /dev/null +++ b/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.purs @@ -0,0 +1,9 @@ +module Main where + +testGuarded :: Boolean -> Int +testGuarded true | false = 1 +testGuarded false = 0 + +testGuardedBoth :: Boolean -> Int +testGuardedBoth true | true = 1 +testGuardedBoth false | true = 0 diff --git a/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.snap b/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.snap new file mode 100644 index 00000000..da3490a7 --- /dev/null +++ b/tests-integration/fixtures/checking2/172_exhaustive_equation_guarded/Main.snap @@ -0,0 +1,24 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testGuarded :: Boolean -> Int +testGuardedBoth :: Boolean -> Int + +Types + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(4), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.purs b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.purs new file mode 100644 index 00000000..c8b08014 --- /dev/null +++ b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.purs @@ -0,0 +1,17 @@ +module Main where + +data Maybe a = Just a | Nothing + +test :: Int +test = + let + f :: Maybe Int -> Int + f (Just _) = 1 + in f Nothing + +test2 :: Int +test2 = + let + g :: Maybe Int -> Int + g Nothing = 1 + in g (Just 42) diff --git a/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap new file mode 100644 index 00000000..301ad400 --- /dev/null +++ b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap @@ -0,0 +1,16 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test :: Int +test2 :: Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] diff --git a/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.purs b/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.purs new file mode 100644 index 00000000..8527ab12 --- /dev/null +++ b/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.purs @@ -0,0 +1,7 @@ +module Main where + +class C a where + c :: a -> Int + +instance cBool :: C Boolean where + c true = 1 diff --git a/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.snap b/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.snap new file mode 100644 index 00000000..df261558 --- /dev/null +++ b/tests-integration/fixtures/checking2/174_exhaustive_instance_equation/Main.snap @@ -0,0 +1,31 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +c :: forall (a :: Type). C (a :: Type) => (a :: Type) -> Int + +Types +C :: Type -> Constraint + +Classes +class forall (a :: Type). C (a :: Type) + c :: forall (a :: Type). C (a :: Type) => (a :: Type) -> Int + +Instances +instance C Boolean + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.purs b/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.purs new file mode 100644 index 00000000..1d48a7d0 --- /dev/null +++ b/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.purs @@ -0,0 +1,19 @@ +module Main where + +data Maybe a = Just a | Nothing + +test1 :: { x :: Int, y :: Int } -> Int +test1 { x, y } = x + +test2 :: { x :: Maybe Int } -> Int +test2 { x: Just n } = n + +test3 :: { x :: Int, y :: Int } -> Int +test3 { x, y } = x +test3 r = 0 + +test4 :: { a :: Maybe Int, b :: Maybe String } -> Int +test4 { a: Just n, b: Just _ } = n + +test5 :: { inner :: { x :: Int } } -> Int +test5 { inner: { x } } = x diff --git a/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.snap b/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.snap new file mode 100644 index 00000000..c9b54cbc --- /dev/null +++ b/tests-integration/fixtures/checking2/175_record_constructor_exhaustive/Main.snap @@ -0,0 +1,57 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +test1 :: { x :: Int, y :: Int } -> Int +test2 :: { x :: Maybe Int } -> Int +test3 :: { x :: Int, y :: Int } -> Int +test4 :: { a :: Maybe Int, b :: Maybe String } -> Int +test5 :: { inner :: { x :: Int } } -> Int + +Types +Maybe :: Type -> Type + +Roles +Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + ], +} +CheckError { + kind: RedundantPatterns { + patterns: [ + "_", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(8), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/176_array_exhaustive/Main.purs b/tests-integration/fixtures/checking2/176_array_exhaustive/Main.purs new file mode 100644 index 00000000..1a4854ae --- /dev/null +++ b/tests-integration/fixtures/checking2/176_array_exhaustive/Main.purs @@ -0,0 +1,15 @@ +module Main where + +testNonExhaustive :: Array Int -> Int +testNonExhaustive [] = 0 +testNonExhaustive [x] = x + +testRedundant :: Array Int -> Int +testRedundant [x] = x +testRedundant [y] = y +testRedundant _ = 0 + +testExhaustive :: Array Int -> Int +testExhaustive [] = 0 +testExhaustive [x] = x +testExhaustive _ = 0 diff --git a/tests-integration/fixtures/checking2/176_array_exhaustive/Main.snap b/tests-integration/fixtures/checking2/176_array_exhaustive/Main.snap new file mode 100644 index 00000000..213f75a7 --- /dev/null +++ b/tests-integration/fixtures/checking2/176_array_exhaustive/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +testNonExhaustive :: Array Int -> Int +testRedundant :: Array Int -> Int +testExhaustive :: Array Int -> Int + +Types + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(5), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} +CheckError { + kind: RedundantPatterns { + patterns: [ + "[_]", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.purs b/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.purs new file mode 100644 index 00000000..c8c99ad5 --- /dev/null +++ b/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.purs @@ -0,0 +1,27 @@ +module Main where + +import Data.Boolean (otherwise) + +foreign import lessThan :: Int -> Int -> Boolean + +test :: Int -> Int +test x = case x of + n + | lessThan n 0 -> 0 + | otherwise -> n + +test' x = case x of + n + | lessThan n 0 -> 0 + | otherwise -> n + +test2 :: Int -> Int +test2 x = case x of + n + | lessThan n 0 -> 0 + | true -> n + +test2' x = case x of + n + | lessThan n 0 -> 0 + | true -> n diff --git a/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.snap b/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.snap new file mode 100644 index 00000000..dffef2be --- /dev/null +++ b/tests-integration/fixtures/checking2/177_exhaustive_guards_otherwise_true/Main.snap @@ -0,0 +1,13 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +lessThan :: Int -> Int -> Boolean +test :: Int -> Int +test' :: Int -> Int +test2 :: Int -> Int +test2' :: Int -> Int + +Types diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 21f64e2b..88049e31 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -365,3 +365,19 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_168_do_final_let_main() { run_test("168_do_final_let", "Main"); } #[rustfmt::skip] #[test] fn test_169_exhaustive_multiple_main() { run_test("169_exhaustive_multiple", "Main"); } + +#[rustfmt::skip] #[test] fn test_170_exhaustive_tuple_main() { run_test("170_exhaustive_tuple", "Main"); } + +#[rustfmt::skip] #[test] fn test_171_exhaustive_equation_redundant_main() { run_test("171_exhaustive_equation_redundant", "Main"); } + +#[rustfmt::skip] #[test] fn test_172_exhaustive_equation_guarded_main() { run_test("172_exhaustive_equation_guarded", "Main"); } + +#[rustfmt::skip] #[test] fn test_173_exhaustive_let_equation_main() { run_test("173_exhaustive_let_equation", "Main"); } + +#[rustfmt::skip] #[test] fn test_174_exhaustive_instance_equation_main() { run_test("174_exhaustive_instance_equation", "Main"); } + +#[rustfmt::skip] #[test] fn test_175_record_constructor_exhaustive_main() { run_test("175_record_constructor_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_176_array_exhaustive_main() { run_test("176_array_exhaustive", "Main"); } + +#[rustfmt::skip] #[test] fn test_177_exhaustive_guards_otherwise_true_main() { run_test("177_exhaustive_guards_otherwise_true", "Main"); } From 8d50602c1bbaa4bcb751e2b67b534aaedf0b00e3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 01:27:54 +0800 Subject: [PATCH 357/386] Fix missing let binding exhaustiveness --- .../checking2/src/source/terms/form_let.rs | 4 ++ .../173_exhaustive_let_equation/Main.snap | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 37e83d90..05002673 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -156,6 +156,10 @@ where function, &name.equations, )?; + + let exhaustiveness = + exhaustive::check_equation_patterns(state, context, &arguments, &name.equations)?; + state.report_exhaustiveness(context, exhaustiveness); } else { // Keep simple let bindings e.g. `bind = ibind` polymorphic. if let [equation] = name.equations.as_ref() diff --git a/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap index 301ad400..00a7b33b 100644 --- a/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap +++ b/tests-integration/fixtures/checking2/173_exhaustive_let_equation/Main.snap @@ -14,3 +14,41 @@ Maybe :: Type -> Type Roles Maybe = [Representational] + +Errors +CheckError { + kind: MissingPatterns { + patterns: [ + Id(7), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(15), + ), + CheckingLetName( + Idx::(0), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(8), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(3), + ), + CheckingExpression( + AstId(40), + ), + CheckingLetName( + Idx::(1), + ), + ], +} From 487c81e950874424d6dde50bc33f0565558c38c8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 02:01:38 +0800 Subject: [PATCH 358/386] Simplify overloaded equation rules --- .../checking2/src/source/term_items.rs | 60 +++++++++++++++---- .../checking2/src/source/terms/equations.rs | 60 ++++++++++++------- .../checking2/src/source/terms/form_let.rs | 42 +++++++------ 3 files changed, 112 insertions(+), 50 deletions(-) diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index a75b9efa..7086edac 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -300,21 +300,41 @@ where } } - equations::check_equations_with( + let equation_set = equations::analyse_equation_set( state, context, - equations::EquationTypeOrigin::Explicit(signature_id), - signature_member_type, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Explicit(signature_id), + expected_type: signature_member_type, + }, &member.equations, - )? + )?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &member.equations, + )?; + state.report_exhaustiveness(context, exhaustiveness); + state.solve_constraints(context)? } else if let Some(specialised_type) = class_member_type { - equations::check_equations_with( + let equation_set = equations::analyse_equation_set( state, context, - equations::EquationTypeOrigin::Implicit, - specialised_type, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Implicit, + expected_type: specialised_type, + }, &member.equations, - )? + )?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &member.equations, + )?; + state.report_exhaustiveness(context, exhaustiveness); + state.solve_constraints(context)? } else { vec![] }; @@ -558,13 +578,19 @@ fn check_value_group_core_check( where Q: ExternalQueries, { - equations::check_equations_with( + let equation_set = equations::analyse_equation_set( state, context, - equations::EquationTypeOrigin::Explicit(signature_id), - signature_type, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Explicit(signature_id), + expected_type: signature_type, + }, equations, - ) + )?; + let exhaustiveness = + equations::compute_equation_exhaustiveness(state, context, &equation_set, equations)?; + state.report_exhaustiveness(context, exhaustiveness); + state.solve_constraints(context) } fn check_value_group_core_infer( @@ -578,7 +604,15 @@ where { let group_type = state.fresh_unification(context.queries, context.prim.t); state.checked.terms.insert(item_id, group_type); - equations::infer_equations_core(state, context, group_type, equations)?; + let equation_set = equations::analyse_equation_set( + state, + context, + equations::EquationMode::Infer { group_type }, + equations, + )?; + let exhaustiveness = + equations::compute_equation_exhaustiveness(state, context, &equation_set, equations)?; + state.report_exhaustiveness(context, exhaustiveness); state.solve_constraints(context) } diff --git a/compiler-core/checking2/src/source/terms/equations.rs b/compiler-core/checking2/src/source/terms/equations.rs index de8142f3..7bbcc222 100644 --- a/compiler-core/checking2/src/source/terms/equations.rs +++ b/compiler-core/checking2/src/source/terms/equations.rs @@ -15,29 +15,53 @@ pub enum EquationTypeOrigin { Implicit, } -pub fn check_equations_with( +pub enum EquationMode { + Check { origin: EquationTypeOrigin, expected_type: TypeId }, + Infer { group_type: TypeId }, +} + +pub struct EquationSet { + pub arguments: Vec, +} + +pub fn analyse_equation_set( state: &mut CheckState, context: &CheckContext, - origin: EquationTypeOrigin, - expected_type: TypeId, + mode: EquationMode, equations: &[lowering::Equation], -) -> QueryResult> +) -> QueryResult where Q: ExternalQueries, { - let required = equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); + match mode { + EquationMode::Check { origin, expected_type } => { + let required = + equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - let signature::DecomposedSignature { arguments, result, .. } = - signature::expect_signature_patterns(state, context, expected_type, required)?; + let signature::DecomposedSignature { arguments, result, .. } = + signature::expect_signature_patterns(state, context, expected_type, required)?; - let function = context.intern_function_chain(&arguments, result); - check_equations_core(state, context, origin, &arguments, result, function, equations)?; + let function = context.intern_function_chain(&arguments, result); + check_equations_core(state, context, origin, &arguments, result, function, equations)?; - let exhaustiveness = - exhaustive::check_equation_patterns(state, context, &arguments, equations)?; - state.report_exhaustiveness(context, exhaustiveness); + Ok(EquationSet { arguments }) + } + EquationMode::Infer { group_type } => { + infer_equation_set(state, context, group_type, equations) + } + } +} - state.solve_constraints(context) +pub fn compute_equation_exhaustiveness( + state: &mut CheckState, + context: &CheckContext, + set: &EquationSet, + equations: &[lowering::Equation], +) -> QueryResult +where + Q: ExternalQueries, +{ + exhaustive::check_equation_patterns(state, context, &set.arguments, equations) } /// Infers the type of value group equations. @@ -45,12 +69,12 @@ where /// For each equation: infer binders, create a result unification variable, /// build the function type, and subtype against `group_type`. Then infer /// the guarded expression and subtype against the result type. -pub fn infer_equations_core( +fn infer_equation_set( state: &mut CheckState, context: &CheckContext, group_type: TypeId, equations: &[lowering::Equation], -) -> QueryResult<()> +) -> QueryResult where Q: ExternalQueries, { @@ -79,11 +103,7 @@ where let toolkit::InspectFunction { arguments, .. } = toolkit::inspect_function(state, context, group_type)?; - let exhaustiveness = - exhaustive::check_equation_patterns(state, context, &arguments, equations)?; - state.report_exhaustiveness(context, exhaustiveness); - - Ok(()) + Ok(EquationSet { arguments }) } /// Checks value group equations against a signature. diff --git a/compiler-core/checking2/src/source/terms/form_let.rs b/compiler-core/checking2/src/source/terms/form_let.rs index 05002673..d60c937b 100644 --- a/compiler-core/checking2/src/source/terms/form_let.rs +++ b/compiler-core/checking2/src/source/terms/form_let.rs @@ -2,7 +2,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{exhaustive, signature, toolkit}; +use crate::core::{exhaustive, toolkit}; use crate::error::ErrorCrumb; use crate::source::terms::equations; use crate::source::{binder, types}; @@ -139,26 +139,22 @@ where }; if let Some(signature_id) = name.signature { - let required = - name.equations.iter().map(|equation| equation.binders.len()).max().unwrap_or(0); - - let signature::DecomposedSignature { arguments, result, .. } = - signature::expect_signature_patterns(state, context, name_type, required)?; - - let function = context.intern_function_chain(&arguments, result); - - equations::check_equations_core( + let equation_set = equations::analyse_equation_set( state, context, - equations::EquationTypeOrigin::Explicit(signature_id), - &arguments, - result, - function, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Explicit(signature_id), + expected_type: name_type, + }, &name.equations, )?; - let exhaustiveness = - exhaustive::check_equation_patterns(state, context, &arguments, &name.equations)?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &name.equations, + )?; state.report_exhaustiveness(context, exhaustiveness); } else { // Keep simple let bindings e.g. `bind = ibind` polymorphic. @@ -169,7 +165,19 @@ where let inferred_type = equations::infer_guarded_expression(state, context, guarded)?; state.checked.nodes.lets.insert(id, inferred_type); } else { - equations::infer_equations_core(state, context, name_type, &name.equations)?; + let equation_set = equations::analyse_equation_set( + state, + context, + equations::EquationMode::Infer { group_type: name_type }, + &name.equations, + )?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &name.equations, + )?; + state.report_exhaustiveness(context, exhaustiveness); } } From c4665fe656d22eb3b7dd46acc6ec57426bd739ed Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 02:34:10 +0800 Subject: [PATCH 359/386] Add test coverage for application and type application --- .../178_application_infix_chain/Main.purs | 57 ++++++++++++++ .../178_application_infix_chain/Main.snap | 33 ++++++++ .../Main.purs | 14 ++++ .../Main.snap | 25 ++++++ .../Main.purs | 23 ++++++ .../Main.snap | 76 +++++++++++++++++++ .../Main.purs | 3 + .../Main.snap | 40 ++++++++++ .../Main.purs | 3 + .../Main.snap | 40 ++++++++++ .../tests/checking2/generated.rs | 10 +++ 11 files changed, 324 insertions(+) create mode 100644 tests-integration/fixtures/checking2/178_application_infix_chain/Main.purs create mode 100644 tests-integration/fixtures/checking2/178_application_infix_chain/Main.snap create mode 100644 tests-integration/fixtures/checking2/179_application_function_subtype/Main.purs create mode 100644 tests-integration/fixtures/checking2/179_application_function_subtype/Main.snap create mode 100644 tests-integration/fixtures/checking2/180_application_function_decomposition/Main.purs create mode 100644 tests-integration/fixtures/checking2/180_application_function_decomposition/Main.snap create mode 100644 tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.purs create mode 100644 tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.snap create mode 100644 tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.purs create mode 100644 tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.snap diff --git a/tests-integration/fixtures/checking2/178_application_infix_chain/Main.purs b/tests-integration/fixtures/checking2/178_application_infix_chain/Main.purs new file mode 100644 index 00000000..b8d16da5 --- /dev/null +++ b/tests-integration/fixtures/checking2/178_application_infix_chain/Main.purs @@ -0,0 +1,57 @@ +module Main where + +add :: Int -> Int -> Int +add x y = x + +sub :: Int -> Int -> Int +sub x y = x + +mul :: Int -> Int -> Int +mul x y = x + +test1 :: Int +test1 = 1 `add` 2 + +test1' = 1 `add` 2 + +test2 :: Int +test2 = 1 `add` 2 `sub` 3 + +test2' = 1 `add` 2 `sub` 3 + +test3 :: Int +test3 = 1 `add` 2 `sub` 3 `mul` 4 + +test3' = 1 `add` 2 `sub` 3 `mul` 4 + +const :: forall a b. a -> b -> a +const x y = x + +test4 :: Int +test4 = 1 `const` "hello" + +test4' = 1 `const` "hello" + +test5 :: Int +test5 = 1 `const` "hello" `add` 2 + +test5' = 1 `const` "hello" `add` 2 + +apply :: forall a b. (a -> b) -> a -> b +apply f x = f x + +chain :: forall a b c. (b -> c) -> (a -> b) -> a -> c +chain g f x = g (f x) + +test6 :: Int +test6 = (\x -> x) `chain` (\x -> x) `apply` 1 + +test6' = (\x -> x) `chain` (\x -> x) `apply` 1 + +curry :: forall a b c. (a -> b -> c) -> a -> b -> c +curry f x y = f x y + +test7 :: Int -> Int +test7 = add `curry` 1 + +test7' = add `curry` 1 diff --git a/tests-integration/fixtures/checking2/178_application_infix_chain/Main.snap b/tests-integration/fixtures/checking2/178_application_infix_chain/Main.snap new file mode 100644 index 00000000..e1875764 --- /dev/null +++ b/tests-integration/fixtures/checking2/178_application_infix_chain/Main.snap @@ -0,0 +1,33 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +add :: Int -> Int -> Int +sub :: Int -> Int -> Int +mul :: Int -> Int -> Int +test1 :: Int +test1' :: Int +test2 :: Int +test2' :: Int +test3 :: Int +test3' :: Int +const :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) -> (a :: Type) +test4 :: Int +test4' :: Int +test5 :: Int +test5' :: Int +apply :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) +chain :: + forall (a :: Type) (b :: Type) (c :: Type). + ((b :: Type) -> (c :: Type)) -> ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (c :: Type) +test6 :: Int +test6' :: Int +curry :: + forall (a :: Type) (b :: Type) (c :: Type). + ((a :: Type) -> (b :: Type) -> (c :: Type)) -> (a :: Type) -> (b :: Type) -> (c :: Type) +test7 :: Int -> Int +test7' :: Int -> Int + +Types diff --git a/tests-integration/fixtures/checking2/179_application_function_subtype/Main.purs b/tests-integration/fixtures/checking2/179_application_function_subtype/Main.purs new file mode 100644 index 00000000..a7750080 --- /dev/null +++ b/tests-integration/fixtures/checking2/179_application_function_subtype/Main.purs @@ -0,0 +1,14 @@ +module Main where + +class Category :: forall k. (k -> k -> Type) -> Constraint +class Category a where + identity :: forall t. a t t + +instance categoryFn :: Category (->) where + identity x = x + +test :: (->) Int Int +test = identity + +test' :: Int -> Int +test' = identity diff --git a/tests-integration/fixtures/checking2/179_application_function_subtype/Main.snap b/tests-integration/fixtures/checking2/179_application_function_subtype/Main.snap new file mode 100644 index 00000000..899c6ea1 --- /dev/null +++ b/tests-integration/fixtures/checking2/179_application_function_subtype/Main.snap @@ -0,0 +1,25 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +identity :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)). + Category @(k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) => + (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)) (t :: (k :: Type)) +test :: Function Int Int +test' :: Int -> Int + +Types +Category :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> Constraint + +Classes +class forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type). Category @(k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) + identity :: + forall (k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)). + Category @(k :: Type) (a :: (k :: Type) -> (k :: Type) -> Type) => + (a :: (k :: Type) -> (k :: Type) -> Type) (t :: (k :: Type)) (t :: (k :: Type)) + +Instances +instance Category @Type Function diff --git a/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.purs b/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.purs new file mode 100644 index 00000000..10c45f83 --- /dev/null +++ b/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.purs @@ -0,0 +1,23 @@ +module Main where + +import Data.Semigroupoid ((<<<)) + +data Maybe a = Just a | Nothing + +class Foldable f where + foldMap :: forall a m. (a -> m) -> f a -> m + +class Foldable f <= FoldableWithIndex i f where + foldMapWithIndex :: forall a m. (i -> a -> m) -> f a -> m + foldlWithIndex :: forall a b. (i -> b -> a -> b) -> b -> f a -> b + foldrWithIndex :: forall a b. (i -> a -> b -> b) -> b -> f a -> b + +data NonEmpty f a = NonEmpty a (f a) + +instance Foldable f => Foldable (NonEmpty f) where + foldMap f (NonEmpty a fa) = f a + +instance FoldableWithIndex i f => FoldableWithIndex (Maybe i) (NonEmpty f) where + foldMapWithIndex f (NonEmpty a fa) = f Nothing a + foldlWithIndex f b (NonEmpty a fa) = foldlWithIndex (f <<< Just) (f Nothing b a) fa + foldrWithIndex f b (NonEmpty a fa) = f Nothing a (foldrWithIndex (f <<< Just) b fa) diff --git a/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.snap b/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.snap new file mode 100644 index 00000000..02457097 --- /dev/null +++ b/tests-integration/fixtures/checking2/180_application_function_decomposition/Main.snap @@ -0,0 +1,76 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +foldMap :: + forall (f :: Type -> Type) (a :: Type) (m :: Type). + Foldable (f :: Type -> Type) => + ((a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) +foldMapWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (m :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) +foldlWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (b :: Type) -> (a :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) +foldrWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (b :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) +NonEmpty :: + forall (f :: Type -> Type) (a :: Type). + (a :: Type) -> (f :: Type -> Type) (a :: Type) -> NonEmpty (f :: Type -> Type) (a :: Type) + +Types +Maybe :: Type -> Type +Foldable :: (Type -> Type) -> Constraint +FoldableWithIndex :: Type -> (Type -> Type) -> Constraint +NonEmpty :: (Type -> Type) -> Type -> Type + +Classes +class forall (f :: Type -> Type). Foldable (f :: Type -> Type) + foldMap :: + forall (f :: Type -> Type) (a :: Type) (m :: Type). + Foldable (f :: Type -> Type) => + ((a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) +class forall (i :: Type) (f :: Type -> Type). Foldable (f :: Type -> Type) <= FoldableWithIndex (i :: Type) (f :: Type -> Type) + foldMapWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (m :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (m :: Type)) -> (f :: Type -> Type) (a :: Type) -> (m :: Type) + foldlWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (b :: Type) -> (a :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) + foldrWithIndex :: + forall (i :: Type) (f :: Type -> Type) (a :: Type) (b :: Type). + FoldableWithIndex (i :: Type) (f :: Type -> Type) => + ((i :: Type) -> (a :: Type) -> (b :: Type) -> (b :: Type)) -> + (b :: Type) -> + (f :: Type -> Type) (a :: Type) -> + (b :: Type) + +Instances +instance forall (t14 :: Type -> Type). + Foldable (t14 :: Type -> Type) => Foldable (NonEmpty (t14 :: Type -> Type)) +instance forall (t15 :: Type) (t16 :: Type -> Type). + FoldableWithIndex (t15 :: Type) (t16 :: Type -> Type) => + FoldableWithIndex (Maybe (t15 :: Type)) (NonEmpty (t16 :: Type -> Type)) + +Roles +Maybe = [Representational] +NonEmpty = [Representational, Nominal] diff --git a/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.purs b/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.purs new file mode 100644 index 00000000..56548ca0 --- /dev/null +++ b/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.purs @@ -0,0 +1,3 @@ +module Main where + +type Bad = Int String diff --git a/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.snap b/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.snap new file mode 100644 index 00000000..9e9ab968 --- /dev/null +++ b/tests-integration/fixtures/checking2/181_type_application_invalid_basic/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Bad :: ?[cannot apply function type] + +Synonyms +type Bad = Int String + +Errors +CheckError { + kind: InvalidTypeApplication { + function_type: Id(3), + function_kind: Id(4), + argument_type: Id(5), + }, + crumbs: [ + CheckingKind( + AstId(6), + ), + InferringKind( + AstId(6), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(4), + t2: Id(7), + }, + crumbs: [ + CheckingKind( + AstId(6), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.purs b/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.purs new file mode 100644 index 00000000..5929643f --- /dev/null +++ b/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.purs @@ -0,0 +1,3 @@ +module Main where + +type Bad = Array Int String diff --git a/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.snap b/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.snap new file mode 100644 index 00000000..ad93cbd2 --- /dev/null +++ b/tests-integration/fixtures/checking2/182_type_application_invalid_too_many/Main.snap @@ -0,0 +1,40 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +Bad :: ?[cannot apply function type] + +Synonyms +type Bad = Array Int String + +Errors +CheckError { + kind: InvalidTypeApplication { + function_type: Id(3), + function_kind: Id(4), + argument_type: Id(5), + }, + crumbs: [ + CheckingKind( + AstId(6), + ), + InferringKind( + AstId(6), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(4), + t2: Id(7), + }, + crumbs: [ + CheckingKind( + AstId(6), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 88049e31..e80bb865 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -381,3 +381,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_176_array_exhaustive_main() { run_test("176_array_exhaustive", "Main"); } #[rustfmt::skip] #[test] fn test_177_exhaustive_guards_otherwise_true_main() { run_test("177_exhaustive_guards_otherwise_true", "Main"); } + +#[rustfmt::skip] #[test] fn test_178_application_infix_chain_main() { run_test("178_application_infix_chain", "Main"); } + +#[rustfmt::skip] #[test] fn test_179_application_function_subtype_main() { run_test("179_application_function_subtype", "Main"); } + +#[rustfmt::skip] #[test] fn test_180_application_function_decomposition_main() { run_test("180_application_function_decomposition", "Main"); } + +#[rustfmt::skip] #[test] fn test_181_type_application_invalid_basic_main() { run_test("181_type_application_invalid_basic", "Main"); } + +#[rustfmt::skip] #[test] fn test_182_type_application_invalid_too_many_main() { run_test("182_type_application_invalid_too_many", "Main"); } From 6109231b634591abb66182291ca63be662a18528 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 02:35:52 +0800 Subject: [PATCH 360/386] Add test coverage for record expressions and binders --- .../183_record_expression_exact/Main.purs | 4 ++ .../183_record_expression_exact/Main.snap | 9 +++ .../Main.purs | 4 ++ .../Main.snap | 26 ++++++++ .../Main.purs | 4 ++ .../Main.snap | 26 ++++++++ .../Main.purs | 4 ++ .../Main.snap | 41 +++++++++++++ .../Main.purs | 4 ++ .../Main.snap | 44 +++++++++++++ .../188_record_access_sections/Main.purs | 39 ++++++++++++ .../188_record_access_sections/Main.snap | 61 +++++++++++++++++++ .../189_record_update_sections/Main.purs | 33 ++++++++++ .../189_record_update_sections/Main.snap | 60 ++++++++++++++++++ .../190_record_binder_shrinking/Main.purs | 4 ++ .../190_record_binder_shrinking/Main.snap | 9 +++ .../Main.purs | 4 ++ .../Main.snap | 27 ++++++++ .../Main.purs | 4 ++ .../Main.snap | 29 +++++++++ .../tests/checking2/generated.rs | 20 ++++++ 21 files changed, 456 insertions(+) create mode 100644 tests-integration/fixtures/checking2/183_record_expression_exact/Main.purs create mode 100644 tests-integration/fixtures/checking2/183_record_expression_exact/Main.snap create mode 100644 tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.purs create mode 100644 tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.snap create mode 100644 tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.purs create mode 100644 tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.snap create mode 100644 tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.purs create mode 100644 tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.snap create mode 100644 tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.purs create mode 100644 tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.snap create mode 100644 tests-integration/fixtures/checking2/188_record_access_sections/Main.purs create mode 100644 tests-integration/fixtures/checking2/188_record_access_sections/Main.snap create mode 100644 tests-integration/fixtures/checking2/189_record_update_sections/Main.purs create mode 100644 tests-integration/fixtures/checking2/189_record_update_sections/Main.snap create mode 100644 tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.purs create mode 100644 tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.snap create mode 100644 tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.purs create mode 100644 tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.snap create mode 100644 tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.purs create mode 100644 tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.snap diff --git a/tests-integration/fixtures/checking2/183_record_expression_exact/Main.purs b/tests-integration/fixtures/checking2/183_record_expression_exact/Main.purs new file mode 100644 index 00000000..80032018 --- /dev/null +++ b/tests-integration/fixtures/checking2/183_record_expression_exact/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1, b: 2 } diff --git a/tests-integration/fixtures/checking2/183_record_expression_exact/Main.snap b/tests-integration/fixtures/checking2/183_record_expression_exact/Main.snap new file mode 100644 index 00000000..bf1a7efa --- /dev/null +++ b/tests-integration/fixtures/checking2/183_record_expression_exact/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types diff --git a/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.purs b/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.purs new file mode 100644 index 00000000..b38d9667 --- /dev/null +++ b/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1 } diff --git a/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.snap b/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.snap new file mode 100644 index 00000000..2d5e7e90 --- /dev/null +++ b/tests-integration/fixtures/checking2/184_record_expression_missing_field/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types + +Errors +CheckError { + kind: PropertyIsMissing { + labels: [ + "b", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(14), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.purs b/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.purs new file mode 100644 index 00000000..2deff69d --- /dev/null +++ b/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int } +test = { a: 1, b: 2 } diff --git a/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.snap b/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.snap new file mode 100644 index 00000000..34d7ee1d --- /dev/null +++ b/tests-integration/fixtures/checking2/185_record_expression_additional_field/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int } + +Types + +Errors +CheckError { + kind: AdditionalProperty { + labels: [ + "b", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(12), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.purs b/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.purs new file mode 100644 index 00000000..bdd8fb6d --- /dev/null +++ b/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } +test = { a: 1, c: 3 } diff --git a/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.snap b/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.snap new file mode 100644 index 00000000..cc6f11d3 --- /dev/null +++ b/tests-integration/fixtures/checking2/186_record_expression_missing_and_additional/Main.snap @@ -0,0 +1,41 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } + +Types + +Errors +CheckError { + kind: PropertyIsMissing { + labels: [ + "b", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(14), + ), + ], +} +CheckError { + kind: AdditionalProperty { + labels: [ + "c", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(14), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.purs b/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.purs new file mode 100644 index 00000000..e11b1035 --- /dev/null +++ b/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { outer :: { x :: Int } } +test = { outer: { x: 1, y: 2 } } diff --git a/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.snap b/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.snap new file mode 100644 index 00000000..e023167a --- /dev/null +++ b/tests-integration/fixtures/checking2/187_record_expression_nested_additional/Main.snap @@ -0,0 +1,44 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { outer :: { x :: Int } } + +Types + +Errors +CheckError { + kind: AdditionalProperty { + labels: [ + "y", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(14), + ), + CheckingExpression( + AstId(16), + ), + ], +} +CheckError { + kind: AdditionalProperty { + labels: [ + "y", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(14), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/188_record_access_sections/Main.purs b/tests-integration/fixtures/checking2/188_record_access_sections/Main.purs new file mode 100644 index 00000000..cb921c80 --- /dev/null +++ b/tests-integration/fixtures/checking2/188_record_access_sections/Main.purs @@ -0,0 +1,39 @@ +module Main where + +map :: forall a b. (a -> b) -> Array a -> Array b +map f x = [] + +apply :: forall a b. (a -> b) -> a -> b +apply f x = f x + +f :: ({ foo :: Int } -> Int) -> Int +f g = g { foo: 1 } + +test1 = _.prop + +test2 = _.a.b.c + +test3 = _.poly + +test4 = map _.prop + +test5 = f _.foo + +test6 r g = g _.bar + +test7 = \r -> _.nonexistent r + +test8 = (_ _.foo) + +test9 = map (_ _.prop) + +test10 = apply (_.a.b) + +test11 = { a: _, b: _.foo } + +test12 = [_, _.bar] + +test13 = case _ of + _ -> _.prop + +test14 = if _ then _.a else _.b diff --git a/tests-integration/fixtures/checking2/188_record_access_sections/Main.snap b/tests-integration/fixtures/checking2/188_record_access_sections/Main.snap new file mode 100644 index 00000000..1be6a854 --- /dev/null +++ b/tests-integration/fixtures/checking2/188_record_access_sections/Main.snap @@ -0,0 +1,61 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Array (a :: Type) -> Array (b :: Type) +apply :: forall (a :: Type) (b :: Type). ((a :: Type) -> (b :: Type)) -> (a :: Type) -> (b :: Type) +f :: ({ foo :: Int } -> Int) -> Int +test1 :: + forall (t5 :: Type) (t4 :: Row Type). { prop :: (t5 :: Type) | (t4 :: Row Type) } -> (t5 :: Type) +test2 :: + forall (t9 :: Type) (t8 :: Row Type) (t7 :: Row Type) (t6 :: Row Type). + { a :: { b :: { c :: (t9 :: Type) | (t8 :: Row Type) } | (t7 :: Row Type) } + | (t6 :: Row Type) + } -> + (t9 :: Type) +test3 :: + forall (t11 :: Type) (t10 :: Row Type). + { poly :: (t11 :: Type) | (t10 :: Row Type) } -> (t11 :: Type) +test4 :: + forall (t13 :: Type) (t12 :: Row Type). + Array { prop :: (t13 :: Type) | (t12 :: Row Type) } -> Array (t13 :: Type) +test5 :: Int +test6 :: + forall (t17 :: Type) (t16 :: Type) (t15 :: Row Type) (t14 :: Type). + (t17 :: Type) -> + (({ bar :: (t16 :: Type) | (t15 :: Row Type) } -> (t16 :: Type)) -> (t14 :: Type)) -> + (t14 :: Type) +test7 :: + forall (t19 :: Type) (t18 :: Row Type). + { nonexistent :: (t19 :: Type) | (t18 :: Row Type) } -> (t19 :: Type) +test8 :: + forall (t22 :: Type) (t21 :: Row Type) (t20 :: Type). + (({ foo :: (t22 :: Type) | (t21 :: Row Type) } -> (t22 :: Type)) -> (t20 :: Type)) -> + (t20 :: Type) +test9 :: + forall (t25 :: Type) (t24 :: Row Type) (t23 :: Type). + Array (({ prop :: (t25 :: Type) | (t24 :: Row Type) } -> (t25 :: Type)) -> (t23 :: Type)) -> + Array (t23 :: Type) +test10 :: + forall (t28 :: Type) (t27 :: Row Type) (t26 :: Row Type). + { a :: { b :: (t28 :: Type) | (t27 :: Row Type) } | (t26 :: Row Type) } -> (t28 :: Type) +test11 :: + forall (t31 :: Type) (t30 :: Type) (t29 :: Row Type). + (t31 :: Type) -> + { a :: (t31 :: Type), b :: { foo :: (t30 :: Type) | (t29 :: Row Type) } -> (t30 :: Type) } +test12 :: + forall (t33 :: Type) (t32 :: Row Type). + ({ bar :: (t33 :: Type) | (t32 :: Row Type) } -> (t33 :: Type)) -> + Array ({ bar :: (t33 :: Type) | (t32 :: Row Type) } -> (t33 :: Type)) +test13 :: + forall (t36 :: Type) (t35 :: Type) (t34 :: Row Type). + (t36 :: Type) -> { prop :: (t35 :: Type) | (t34 :: Row Type) } -> (t35 :: Type) +test14 :: + forall (t38 :: Type) (t37 :: Row Type). + Boolean -> { a :: (t38 :: Type), b :: (t38 :: Type) | (t37 :: Row Type) } -> (t38 :: Type) + +Types diff --git a/tests-integration/fixtures/checking2/189_record_update_sections/Main.purs b/tests-integration/fixtures/checking2/189_record_update_sections/Main.purs new file mode 100644 index 00000000..5a234a2c --- /dev/null +++ b/tests-integration/fixtures/checking2/189_record_update_sections/Main.purs @@ -0,0 +1,33 @@ +module Main where + +singleFieldUpdate = _ { a = 1 } + +multipleFieldUpdate = _ { a = 2, b = true, c = "three" } + +nestedRecordUpdate = _ { a { b { c = 42 } } } + +updateWithValueSection = _ { a = (_ + 1) } + +updateWithSectionInteraction = _ { a = _ } + +polymorphicRecordUpdate = _ { foo = 0 } + +higherOrderContext = map (_ { x = 10 }) + +multipleSectionsInteraction = _ { x = _, y = _ } + +nestedSectionInteraction = _ { a { b = _ } } + +mixedSections = _ { a = _ + 1, b = 2 } + +recordAccessSectionUpdate = _ { a = _.b } + +concreteRecordUpdateSection = ({ a: 1 } { a = _ }) + +map :: forall a b. (a -> b) -> Array a -> Array b +map f x = [] + +infixl 6 add as + + +add :: Int -> Int -> Int +add x y = x diff --git a/tests-integration/fixtures/checking2/189_record_update_sections/Main.snap b/tests-integration/fixtures/checking2/189_record_update_sections/Main.snap new file mode 100644 index 00000000..5f9956e1 --- /dev/null +++ b/tests-integration/fixtures/checking2/189_record_update_sections/Main.snap @@ -0,0 +1,60 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +singleFieldUpdate :: + forall (t1 :: Type) (t0 :: Row Type). + { a :: (t1 :: Type) | (t0 :: Row Type) } -> { a :: Int | (t0 :: Row Type) } +multipleFieldUpdate :: + forall (t5 :: Type) (t4 :: Type) (t3 :: Type) (t2 :: Row Type). + { a :: (t5 :: Type), b :: (t4 :: Type), c :: (t3 :: Type) | (t2 :: Row Type) } -> + { a :: Int, b :: Boolean, c :: String | (t2 :: Row Type) } +nestedRecordUpdate :: + forall (t9 :: Type) (t8 :: Row Type) (t7 :: Row Type) (t6 :: Row Type). + { a :: { b :: { c :: (t9 :: Type) | (t8 :: Row Type) } | (t7 :: Row Type) } + | (t6 :: Row Type) + } -> + { a :: { b :: { c :: Int | (t8 :: Row Type) } | (t7 :: Row Type) } | (t6 :: Row Type) } +updateWithValueSection :: + forall (t11 :: Type) (t10 :: Row Type). + { a :: (t11 :: Type) | (t10 :: Row Type) } -> { a :: Int -> Int | (t10 :: Row Type) } +updateWithSectionInteraction :: + forall (t14 :: Type) (t13 :: Row Type) (t12 :: Type). + { a :: (t14 :: Type) | (t13 :: Row Type) } -> + (t12 :: Type) -> + { a :: (t12 :: Type) | (t13 :: Row Type) } +polymorphicRecordUpdate :: + forall (t16 :: Type) (t15 :: Row Type). + { foo :: (t16 :: Type) | (t15 :: Row Type) } -> { foo :: Int | (t15 :: Row Type) } +higherOrderContext :: + forall (t20 :: Type) (t19 :: Row Type). + Array { x :: (t20 :: Type) | (t19 :: Row Type) } -> Array { x :: Int | (t19 :: Row Type) } +multipleSectionsInteraction :: + forall (t25 :: Type) (t24 :: Type) (t23 :: Row Type) (t22 :: Type) (t21 :: Type). + { x :: (t25 :: Type), y :: (t24 :: Type) | (t23 :: Row Type) } -> + (t22 :: Type) -> + (t21 :: Type) -> + { x :: (t22 :: Type), y :: (t21 :: Type) | (t23 :: Row Type) } +nestedSectionInteraction :: + forall (t29 :: Type) (t28 :: Row Type) (t27 :: Row Type) (t26 :: Type). + { a :: { b :: (t29 :: Type) | (t28 :: Row Type) } | (t27 :: Row Type) } -> + (t26 :: Type) -> + { a :: { b :: (t26 :: Type) | (t28 :: Row Type) } | (t27 :: Row Type) } +mixedSections :: + forall (t32 :: Type) (t31 :: Type) (t30 :: Row Type). + { a :: (t32 :: Type), b :: (t31 :: Type) | (t30 :: Row Type) } -> + { a :: Int -> Int, b :: Int | (t30 :: Row Type) } +recordAccessSectionUpdate :: + forall (t36 :: Type) (t35 :: Row Type) (t34 :: Type) (t33 :: Row Type). + { a :: (t36 :: Type) | (t35 :: Row Type) } -> + { a :: { b :: (t34 :: Type) | (t33 :: Row Type) } -> (t34 :: Type) | (t35 :: Row Type) } +concreteRecordUpdateSection :: forall (t37 :: Type). (t37 :: Type) -> { a :: (t37 :: Type) } +map :: + forall (a :: Type) (b :: Type). + ((a :: Type) -> (b :: Type)) -> Array (a :: Type) -> Array (b :: Type) ++ :: Int -> Int -> Int +add :: Int -> Int -> Int + +Types diff --git a/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.purs b/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.purs new file mode 100644 index 00000000..9a86121a --- /dev/null +++ b/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int, b :: Int } -> Int +test { a } = a diff --git a/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.snap b/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.snap new file mode 100644 index 00000000..518afcfa --- /dev/null +++ b/tests-integration/fixtures/checking2/190_record_binder_shrinking/Main.snap @@ -0,0 +1,9 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int, b :: Int } -> Int + +Types diff --git a/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.purs b/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.purs new file mode 100644 index 00000000..bc79aeef --- /dev/null +++ b/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { a :: Int } -> Int +test { a, b, c } = a diff --git a/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.snap b/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.snap new file mode 100644 index 00000000..789dc9ba --- /dev/null +++ b/tests-integration/fixtures/checking2/191_record_binder_additional_property/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { a :: Int } -> Int + +Types + +Errors +CheckError { + kind: AdditionalProperty { + labels: [ + "b", + "c", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingBinder( + AstId(13), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.purs b/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.purs new file mode 100644 index 00000000..f14006b4 --- /dev/null +++ b/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.purs @@ -0,0 +1,4 @@ +module Main where + +test :: { outer :: { x :: Int } } -> Int +test { outer: { x, y } } = x diff --git a/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.snap b/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.snap new file mode 100644 index 00000000..8e480492 --- /dev/null +++ b/tests-integration/fixtures/checking2/192_record_binder_additional_property_nested/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: { outer :: { x :: Int } } -> Int + +Types + +Errors +CheckError { + kind: AdditionalProperty { + labels: [ + "y", + ], + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingBinder( + AstId(15), + ), + CheckingBinder( + AstId(17), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index e80bb865..afcd3065 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -391,3 +391,23 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_181_type_application_invalid_basic_main() { run_test("181_type_application_invalid_basic", "Main"); } #[rustfmt::skip] #[test] fn test_182_type_application_invalid_too_many_main() { run_test("182_type_application_invalid_too_many", "Main"); } + +#[rustfmt::skip] #[test] fn test_183_record_expression_exact_main() { run_test("183_record_expression_exact", "Main"); } + +#[rustfmt::skip] #[test] fn test_184_record_expression_missing_field_main() { run_test("184_record_expression_missing_field", "Main"); } + +#[rustfmt::skip] #[test] fn test_185_record_expression_additional_field_main() { run_test("185_record_expression_additional_field", "Main"); } + +#[rustfmt::skip] #[test] fn test_186_record_expression_missing_and_additional_main() { run_test("186_record_expression_missing_and_additional", "Main"); } + +#[rustfmt::skip] #[test] fn test_187_record_expression_nested_additional_main() { run_test("187_record_expression_nested_additional", "Main"); } + +#[rustfmt::skip] #[test] fn test_188_record_access_sections_main() { run_test("188_record_access_sections", "Main"); } + +#[rustfmt::skip] #[test] fn test_189_record_update_sections_main() { run_test("189_record_update_sections", "Main"); } + +#[rustfmt::skip] #[test] fn test_190_record_binder_shrinking_main() { run_test("190_record_binder_shrinking", "Main"); } + +#[rustfmt::skip] #[test] fn test_191_record_binder_additional_property_main() { run_test("191_record_binder_additional_property", "Main"); } + +#[rustfmt::skip] #[test] fn test_192_record_binder_additional_property_nested_main() { run_test("192_record_binder_additional_property_nested", "Main"); } From d7fb0fa4ae5d77c686e4d424c38b0c638efd7fd7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 02:36:38 +0800 Subject: [PATCH 361/386] Add test coverage for givens and row instances --- .../checking2/193_givens_matching/Main.purs | 7 ++ .../checking2/193_givens_matching/Main.snap | 15 ++++ .../Main.purs | 13 +++ .../Main.snap | 26 ++++++ .../Main.purs | 22 +++++ .../Main.snap | 18 ++++ .../196_instance_record_matching/Main.purs | 21 +++++ .../196_instance_record_matching/Main.snap | 26 ++++++ .../197_instance_record_open_row/Main.purs | 27 ++++++ .../197_instance_record_open_row/Main.snap | 26 ++++++ .../198_instance_head_invalid_row/Main.purs | 12 +++ .../198_instance_head_invalid_row/Main.snap | 89 +++++++++++++++++++ .../Main.purs | 11 +++ .../Main.snap | 23 +++++ .../Main.purs | 16 ++++ .../Main.snap | 27 ++++++ .../tests/checking2/generated.rs | 16 ++++ 17 files changed, 395 insertions(+) create mode 100644 tests-integration/fixtures/checking2/193_givens_matching/Main.purs create mode 100644 tests-integration/fixtures/checking2/193_givens_matching/Main.snap create mode 100644 tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.purs create mode 100644 tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.snap create mode 100644 tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.purs create mode 100644 tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.snap create mode 100644 tests-integration/fixtures/checking2/196_instance_record_matching/Main.purs create mode 100644 tests-integration/fixtures/checking2/196_instance_record_matching/Main.snap create mode 100644 tests-integration/fixtures/checking2/197_instance_record_open_row/Main.purs create mode 100644 tests-integration/fixtures/checking2/197_instance_record_open_row/Main.snap create mode 100644 tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.purs create mode 100644 tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.snap create mode 100644 tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.purs create mode 100644 tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.snap create mode 100644 tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.purs create mode 100644 tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.snap diff --git a/tests-integration/fixtures/checking2/193_givens_matching/Main.purs b/tests-integration/fixtures/checking2/193_givens_matching/Main.purs new file mode 100644 index 00000000..4b9bc0a6 --- /dev/null +++ b/tests-integration/fixtures/checking2/193_givens_matching/Main.purs @@ -0,0 +1,7 @@ +module Main where + +class Eq a where + eq :: a -> a -> Boolean + +test :: forall a. Eq a => a -> Boolean +test x = eq x x diff --git a/tests-integration/fixtures/checking2/193_givens_matching/Main.snap b/tests-integration/fixtures/checking2/193_givens_matching/Main.snap new file mode 100644 index 00000000..8fafd85d --- /dev/null +++ b/tests-integration/fixtures/checking2/193_givens_matching/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean +test :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> Boolean + +Types +Eq :: Type -> Constraint + +Classes +class forall (a :: Type). Eq (a :: Type) + eq :: forall (a :: Type). Eq (a :: Type) => (a :: Type) -> (a :: Type) -> Boolean diff --git a/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.purs b/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.purs new file mode 100644 index 00000000..89b9c793 --- /dev/null +++ b/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.purs @@ -0,0 +1,13 @@ +module Main where + +class Convert a b | a -> b where + convert :: a -> b + +useGiven :: forall x. Convert Int x => x +useGiven = convert 42 + +class Relate a b c | a b -> c where + relate :: a -> b -> c + +useRelate :: forall y. Relate Int String y => y +useRelate = relate 1 "hello" diff --git a/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.snap b/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.snap new file mode 100644 index 00000000..7fb76aae --- /dev/null +++ b/tests-integration/fixtures/checking2/194_givens_functional_dependency/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +useGiven :: forall (x :: Type). Convert Int (x :: Type) => (x :: Type) +relate :: + forall (a :: Type) (b :: Type) (c :: Type). + Relate (a :: Type) (b :: Type) (c :: Type) => (a :: Type) -> (b :: Type) -> (c :: Type) +useRelate :: forall (y :: Type). Relate Int String (y :: Type) => (y :: Type) + +Types +Convert :: Type -> Type -> Constraint +Relate :: Type -> Type -> Type -> Constraint + +Classes +class forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) + convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +class forall (a :: Type) (b :: Type) (c :: Type). Relate (a :: Type) (b :: Type) (c :: Type) + relate :: + forall (a :: Type) (b :: Type) (c :: Type). + Relate (a :: Type) (b :: Type) (c :: Type) => (a :: Type) -> (b :: Type) -> (c :: Type) diff --git a/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.purs b/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.purs new file mode 100644 index 00000000..01301cf1 --- /dev/null +++ b/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.purs @@ -0,0 +1,22 @@ +module Main where + +import Control.Applicative (class Applicative, pure) +import Control.Bind (class Bind, bind) +import Control.Monad (class Monad) +import Control.Monad.Rec (class MonadRec, tailRecM) +import Data.Functor (class Functor, map) + +test :: forall m a. MonadRec m => a -> m a +test a = go a + where + go x = pure x + +test2 :: forall m a. MonadRec m => m a -> m a +test2 ma = go ma + where + go x = bind x pure + +test3 :: forall m. MonadRec m => m Int -> m Int +test3 mi = go mi + where + go x = map (\y -> y) x diff --git a/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.snap b/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.snap new file mode 100644 index 00000000..da06dd28 --- /dev/null +++ b/tests-integration/fixtures/checking2/195_givens_superclass_where_binding/Main.snap @@ -0,0 +1,18 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: + forall (m :: Type -> Type) (a :: Type). + MonadRec (m :: Type -> Type) => (a :: Type) -> (m :: Type -> Type) (a :: Type) +test2 :: + forall (m :: Type -> Type) (a :: Type). + MonadRec (m :: Type -> Type) => + (m :: Type -> Type) (a :: Type) -> (m :: Type -> Type) (a :: Type) +test3 :: + forall (m :: Type -> Type). + MonadRec (m :: Type -> Type) => (m :: Type -> Type) Int -> (m :: Type -> Type) Int + +Types diff --git a/tests-integration/fixtures/checking2/196_instance_record_matching/Main.purs b/tests-integration/fixtures/checking2/196_instance_record_matching/Main.purs new file mode 100644 index 00000000..1d49266a --- /dev/null +++ b/tests-integration/fixtures/checking2/196_instance_record_matching/Main.purs @@ -0,0 +1,21 @@ +module Main where + +class Make :: Type -> Type -> Constraint +class Make a b | a -> b where + make :: a -> b + +instance Make { | r } { | r } where + make x = x + +testMake :: { a :: Int, b :: String } -> { a :: Int, b :: String } +testMake = make + +class Convert :: Type -> Type -> Constraint +class Convert a b | a -> b where + convert :: a -> b + +instance Convert { | r } { converted :: { | r } } where + convert x = { converted: x } + +testConvert :: { x :: Int } -> { converted :: { x :: Int } } +testConvert = convert diff --git a/tests-integration/fixtures/checking2/196_instance_record_matching/Main.snap b/tests-integration/fixtures/checking2/196_instance_record_matching/Main.snap new file mode 100644 index 00000000..b3cab8ee --- /dev/null +++ b/tests-integration/fixtures/checking2/196_instance_record_matching/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +make :: forall (a :: Type) (b :: Type). Make (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testMake :: { a :: Int, b :: String } -> { a :: Int, b :: String } +convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testConvert :: { x :: Int } -> { converted :: { x :: Int } } + +Types +Make :: Type -> Type -> Constraint +Convert :: Type -> Type -> Constraint + +Classes +class forall (a :: Type) (b :: Type). Make (a :: Type) (b :: Type) + make :: forall (a :: Type) (b :: Type). Make (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +class forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) + convert :: + forall (a :: Type) (b :: Type). Convert (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) + +Instances +instance forall (t4 :: Row Type). Make {| (t4 :: Row Type) } {| (t4 :: Row Type) } +instance forall (t5 :: Row Type). Convert {| (t5 :: Row Type) } { converted :: {| (t5 :: Row Type) } } diff --git a/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.purs b/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.purs new file mode 100644 index 00000000..146f6187 --- /dev/null +++ b/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.purs @@ -0,0 +1,27 @@ +module Main where + +class Clone :: Type -> Type -> Constraint +class Clone a b | a -> b where + clone :: a -> b + +instance Clone { | r } { | r } where + clone x = x + +testClonePerson :: { name :: String, age :: Int } -> { name :: String, age :: Int } +testClonePerson = clone + +testCloneEmpty :: {} -> {} +testCloneEmpty = clone + +testCloneSingle :: { x :: Int } -> { x :: Int } +testCloneSingle = clone + +class Nest :: Type -> Type -> Constraint +class Nest a b | a -> b where + nest :: a -> b + +instance Nest { | r } { inner :: { | r }, outer :: Int } where + nest x = { inner: x, outer: 0 } + +testNest :: { a :: String } -> { inner :: { a :: String }, outer :: Int } +testNest = nest diff --git a/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.snap b/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.snap new file mode 100644 index 00000000..dfd20102 --- /dev/null +++ b/tests-integration/fixtures/checking2/197_instance_record_open_row/Main.snap @@ -0,0 +1,26 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +clone :: forall (a :: Type) (b :: Type). Clone (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testClonePerson :: { age :: Int, name :: String } -> { age :: Int, name :: String } +testCloneEmpty :: {} -> {} +testCloneSingle :: { x :: Int } -> { x :: Int } +nest :: forall (a :: Type) (b :: Type). Nest (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +testNest :: { a :: String } -> { inner :: { a :: String }, outer :: Int } + +Types +Clone :: Type -> Type -> Constraint +Nest :: Type -> Type -> Constraint + +Classes +class forall (a :: Type) (b :: Type). Clone (a :: Type) (b :: Type) + clone :: forall (a :: Type) (b :: Type). Clone (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) +class forall (a :: Type) (b :: Type). Nest (a :: Type) (b :: Type) + nest :: forall (a :: Type) (b :: Type). Nest (a :: Type) (b :: Type) => (a :: Type) -> (b :: Type) + +Instances +instance forall (t4 :: Row Type). Clone {| (t4 :: Row Type) } {| (t4 :: Row Type) } +instance forall (t5 :: Row Type). Nest {| (t5 :: Row Type) } { inner :: {| (t5 :: Row Type) }, outer :: Int } diff --git a/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.purs b/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.purs new file mode 100644 index 00000000..461d9b6f --- /dev/null +++ b/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.purs @@ -0,0 +1,12 @@ +module Main where + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +class T :: forall k. k -> Type +class T a + +instance T ( a :: Int ) +instance T { a :: Int } +instance T (Proxy ( a :: Int )) +instance T (Proxy { a :: Int }) diff --git a/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.snap b/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.snap new file mode 100644 index 00000000..35a53ce8 --- /dev/null +++ b/tests-integration/fixtures/checking2/198_instance_head_invalid_row/Main.snap @@ -0,0 +1,89 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type +T :: forall (k :: Type). (k :: Type) -> Type + +Classes +class forall (k :: Type) (a :: (k :: Type)). T @(k :: Type) (a :: (k :: Type)) + +Instances +instance T @(Row Type) ( a :: Int ) +instance T @Type { a :: Int } +instance T @Type (Proxy @(Row Type) ( a :: Int )) +instance T @Type (Proxy @Type { a :: Int }) + +Roles +Proxy = [Phantom] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [], +} +CheckError { + kind: InstanceHeadLabeledRow { + class_file: Idx::(39), + class_item: Idx::(1), + position: 0, + type_message: Id(7), + }, + crumbs: [], +} +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [], +} +CheckError { + kind: InstanceHeadLabeledRow { + class_file: Idx::(39), + class_item: Idx::(1), + position: 0, + type_message: Id(8), + }, + crumbs: [], +} +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [], +} +CheckError { + kind: InstanceHeadLabeledRow { + class_file: Idx::(39), + class_item: Idx::(1), + position: 0, + type_message: Id(9), + }, + crumbs: [], +} +CheckError { + kind: CannotUnify { + t1: Id(5), + t2: Id(6), + }, + crumbs: [], +} +CheckError { + kind: InstanceHeadLabeledRow { + class_file: Idx::(39), + class_item: Idx::(1), + position: 0, + type_message: Id(10), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.purs b/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.purs new file mode 100644 index 00000000..53105fa8 --- /dev/null +++ b/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Data.Newtype (class Newtype, unwrap) + +newtype Endo :: forall k. (k -> k -> Type) -> k -> Type +newtype Endo c a = Endo (c a a) + +instance Newtype (Endo c a) (c a a) + +test :: forall b. Endo Function b -> b -> b +test x = unwrap x diff --git a/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.snap b/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.snap new file mode 100644 index 00000000..624c09ff --- /dev/null +++ b/tests-integration/fixtures/checking2/199_instance_kind_application_matching/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Endo :: + forall (k :: Type) (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)). + (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)) (a :: (k :: Type)) -> + Endo @(k :: Type) (c :: (k :: Type) -> (k :: Type) -> Type) (a :: (k :: Type)) +test :: forall (b :: Type). Endo @Type Function (b :: Type) -> (b :: Type) -> (b :: Type) + +Types +Endo :: forall (k :: Type). ((k :: Type) -> (k :: Type) -> Type) -> (k :: Type) -> Type + +Instances +instance forall (t5 :: Type) (t3 :: (t5 :: Type) -> (t5 :: Type) -> Type) (t4 :: (t5 :: Type)). + Newtype @Type + (Endo @(t5 :: Type) (t3 :: (t5 :: Type) -> (t5 :: Type) -> Type) (t4 :: (t5 :: Type))) + ((t3 :: (t5 :: Type) -> (t5 :: Type) -> Type) (t4 :: (t5 :: Type)) (t4 :: (t5 :: Type))) + +Roles +Endo = [Representational, Nominal] diff --git a/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.purs b/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.purs new file mode 100644 index 00000000..e84b05aa --- /dev/null +++ b/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.purs @@ -0,0 +1,16 @@ +module Main where + +foreign import unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2 + +class BuildRecord :: Row Type -> Row Type -> Constraint +class BuildRecord row subrow | row -> subrow where + buildIt :: Record subrow + +instance buildRecordImpl :: BuildRecord row subrow where + buildIt = result + where + result :: Record subrow + result = unsafeSet "x" 42 {} + +test :: forall r s. BuildRecord r s => Record s +test = buildIt diff --git a/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.snap b/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.snap new file mode 100644 index 00000000..9f72093a --- /dev/null +++ b/tests-integration/fixtures/checking2/200_instance_bound_variable_unification/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeSet :: + forall (r1 :: Row Type) (r2 :: Row Type) (a :: Type). + String -> (a :: Type) -> {| (r1 :: Row Type) } -> {| (r2 :: Row Type) } +buildIt :: + forall (row :: Row Type) (subrow :: Row Type). + BuildRecord (row :: Row Type) (subrow :: Row Type) => {| (subrow :: Row Type) } +test :: + forall (r :: Row Type) (s :: Row Type). + BuildRecord (r :: Row Type) (s :: Row Type) => {| (s :: Row Type) } + +Types +BuildRecord :: Row Type -> Row Type -> Constraint + +Classes +class forall (row :: Row Type) (subrow :: Row Type). BuildRecord (row :: Row Type) (subrow :: Row Type) + buildIt :: + forall (row :: Row Type) (subrow :: Row Type). + BuildRecord (row :: Row Type) (subrow :: Row Type) => {| (subrow :: Row Type) } + +Instances +instance forall (t2 :: Row Type) (t3 :: Row Type). BuildRecord (t2 :: Row Type) (t3 :: Row Type) diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index afcd3065..503053c1 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -411,3 +411,19 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_191_record_binder_additional_property_main() { run_test("191_record_binder_additional_property", "Main"); } #[rustfmt::skip] #[test] fn test_192_record_binder_additional_property_nested_main() { run_test("192_record_binder_additional_property_nested", "Main"); } + +#[rustfmt::skip] #[test] fn test_193_givens_matching_main() { run_test("193_givens_matching", "Main"); } + +#[rustfmt::skip] #[test] fn test_194_givens_functional_dependency_main() { run_test("194_givens_functional_dependency", "Main"); } + +#[rustfmt::skip] #[test] fn test_195_givens_superclass_where_binding_main() { run_test("195_givens_superclass_where_binding", "Main"); } + +#[rustfmt::skip] #[test] fn test_196_instance_record_matching_main() { run_test("196_instance_record_matching", "Main"); } + +#[rustfmt::skip] #[test] fn test_197_instance_record_open_row_main() { run_test("197_instance_record_open_row", "Main"); } + +#[rustfmt::skip] #[test] fn test_198_instance_head_invalid_row_main() { run_test("198_instance_head_invalid_row", "Main"); } + +#[rustfmt::skip] #[test] fn test_199_instance_kind_application_matching_main() { run_test("199_instance_kind_application_matching", "Main"); } + +#[rustfmt::skip] #[test] fn test_200_instance_bound_variable_unification_main() { run_test("200_instance_bound_variable_unification", "Main"); } From 55ac189dde405373963548eedcc3607380d243d1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 02:37:15 +0800 Subject: [PATCH 362/386] Add test coverage for synonyms and binder instantiation --- .../Main.purs | 9 ++ .../Main.snap | 20 ++++ .../202_synonym_forall_expansion/Main.purs | 6 ++ .../202_synonym_forall_expansion/Main.snap | 34 ++++++ .../203_binder_instantiation/Main.purs | 25 +++++ .../203_binder_instantiation/Main.snap | 100 ++++++++++++++++++ .../tests/checking2/generated.rs | 6 ++ 7 files changed, 200 insertions(+) create mode 100644 tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.purs create mode 100644 tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.snap create mode 100644 tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.purs create mode 100644 tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap create mode 100644 tests-integration/fixtures/checking2/203_binder_instantiation/Main.purs create mode 100644 tests-integration/fixtures/checking2/203_binder_instantiation/Main.snap diff --git a/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.purs b/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.purs new file mode 100644 index 00000000..7b0132c9 --- /dev/null +++ b/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.purs @@ -0,0 +1,9 @@ +module Main where + +data Box (f :: Type -> Type) (a :: Type) = Box (f a) + +type Wrap :: (Type -> Type) -> Type -> Type +type Wrap f = Box f + +test :: forall f. Wrap f Int -> Wrap f Int +test x = x diff --git a/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.snap b/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.snap new file mode 100644 index 00000000..c2af8435 --- /dev/null +++ b/tests-integration/fixtures/checking2/201_synonym_function_result_kind/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Box :: + forall (f :: Type -> Type) (a :: Type). + (f :: Type -> Type) (a :: Type) -> Box (f :: Type -> Type) (a :: Type) +test :: forall (f :: Type -> Type). Wrap (f :: Type -> Type) Int -> Wrap (f :: Type -> Type) Int + +Types +Box :: (Type -> Type) -> Type -> Type +Wrap :: (Type -> Type) -> Type -> Type + +Synonyms +type Wrap f = Box (f :: Type -> Type) + +Roles +Box = [Representational, Nominal] diff --git a/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.purs b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.purs new file mode 100644 index 00000000..9a8c39a5 --- /dev/null +++ b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.purs @@ -0,0 +1,6 @@ +module Main where + +type NatTrans f g = forall a. f a -> g a + +apply :: forall f g. NatTrans f g -> f Int -> g Int +apply nat fa = nat fa diff --git a/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap new file mode 100644 index 00000000..85dcf2df --- /dev/null +++ b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +apply :: + forall (f :: Type -> Type) (g :: Type -> Type). + NatTrans @Type (f :: Type -> Type) (g :: Type -> Type) -> + (f :: Type -> Type) Int -> + (g :: Type -> Type) Int + +Types +NatTrans :: forall (t3 :: Type). ((t3 :: Type) -> Type) -> ((t3 :: Type) -> Type) -> Type + +Synonyms +type NatTrans f g = forall (a :: (t3 :: Type)). + (f :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) -> (g :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) + +Errors +CheckError { + kind: CannotUnify { + t1: Id(9), + t2: Id(10), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(39), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/203_binder_instantiation/Main.purs b/tests-integration/fixtures/checking2/203_binder_instantiation/Main.purs new file mode 100644 index 00000000..ceb764ab --- /dev/null +++ b/tests-integration/fixtures/checking2/203_binder_instantiation/Main.purs @@ -0,0 +1,25 @@ +module Main where + +data Maybe a = Just a | Nothing +data Id = MkId (forall a. a -> a) + +identity :: forall a. Maybe (a -> a) +identity = Nothing + +test :: Partial => Int +test = case identity of + Just f -> let _ = f 42 in f true + +test2 :: Id -> Boolean +test2 x = case x of + MkId f -> let _ = f 42 in f true + +test3 :: Partial => Int +test3 = + let (Just f) = identity + in let _ = f 42 in f true + +test4 :: Id -> Boolean +test4 x = + let (MkId f) = x + in let _ = f 42 in f true diff --git a/tests-integration/fixtures/checking2/203_binder_instantiation/Main.snap b/tests-integration/fixtures/checking2/203_binder_instantiation/Main.snap new file mode 100644 index 00000000..0632a4a7 --- /dev/null +++ b/tests-integration/fixtures/checking2/203_binder_instantiation/Main.snap @@ -0,0 +1,100 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nothing :: forall (a :: Type). Maybe (a :: Type) +MkId :: (forall (a :: Type). (a :: Type) -> (a :: Type)) -> Id +identity :: forall (a :: Type). Maybe ((a :: Type) -> (a :: Type)) +test :: Partial => Int +test2 :: Id -> Boolean +test3 :: Partial => Int +test4 :: Id -> Boolean + +Types +Maybe :: Type -> Type +Id :: Type + +Roles +Maybe = [Representational] +Id = [] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + CheckingExpression( + AstId(38), + ), + CheckingExpression( + AstId(48), + ), + CheckingExpression( + AstId(57), + ), + CheckingExpression( + AstId(60), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(9), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(4), + ), + CheckingExpression( + AstId(38), + ), + ], +} +CheckError { + kind: MissingPatterns { + patterns: [ + Id(9), + ], + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + CheckingExpression( + AstId(100), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(6), + ), + CheckingExpression( + AstId(100), + ), + CheckingExpression( + AstId(108), + ), + CheckingExpression( + AstId(117), + ), + CheckingExpression( + AstId(120), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 503053c1..3a0704df 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -427,3 +427,9 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_199_instance_kind_application_matching_main() { run_test("199_instance_kind_application_matching", "Main"); } #[rustfmt::skip] #[test] fn test_200_instance_bound_variable_unification_main() { run_test("200_instance_bound_variable_unification", "Main"); } + +#[rustfmt::skip] #[test] fn test_201_synonym_function_result_kind_main() { run_test("201_synonym_function_result_kind", "Main"); } + +#[rustfmt::skip] #[test] fn test_202_synonym_forall_expansion_main() { run_test("202_synonym_forall_expansion", "Main"); } + +#[rustfmt::skip] #[test] fn test_203_binder_instantiation_main() { run_test("203_binder_instantiation", "Main"); } From 2316837533b6db37794608712c281cc843e50cf3 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 03:55:19 +0800 Subject: [PATCH 363/386] Add test coverage for structural coercions --- .../checking2/204_coercible_phantom/Main.purs | 11 +++++++++ .../checking2/204_coercible_phantom/Main.snap | 17 ++++++++++++++ .../205_coercible_representational/Main.purs | 18 +++++++++++++++ .../205_coercible_representational/Main.snap | 23 +++++++++++++++++++ .../checking2/206_coercible_array/Main.purs | 11 +++++++++ .../checking2/206_coercible_array/Main.snap | 15 ++++++++++++ .../checking2/207_coercible_record/Main.purs | 16 +++++++++++++ .../checking2/207_coercible_record/Main.snap | 19 +++++++++++++++ .../208_coercible_transitivity/Main.purs | 18 +++++++++++++++ .../208_coercible_transitivity/Main.snap | 20 ++++++++++++++++ .../tests/checking2/generated.rs | 10 ++++++++ 11 files changed, 178 insertions(+) create mode 100644 tests-integration/fixtures/checking2/204_coercible_phantom/Main.purs create mode 100644 tests-integration/fixtures/checking2/204_coercible_phantom/Main.snap create mode 100644 tests-integration/fixtures/checking2/205_coercible_representational/Main.purs create mode 100644 tests-integration/fixtures/checking2/205_coercible_representational/Main.snap create mode 100644 tests-integration/fixtures/checking2/206_coercible_array/Main.purs create mode 100644 tests-integration/fixtures/checking2/206_coercible_array/Main.snap create mode 100644 tests-integration/fixtures/checking2/207_coercible_record/Main.purs create mode 100644 tests-integration/fixtures/checking2/207_coercible_record/Main.snap create mode 100644 tests-integration/fixtures/checking2/208_coercible_transitivity/Main.purs create mode 100644 tests-integration/fixtures/checking2/208_coercible_transitivity/Main.snap diff --git a/tests-integration/fixtures/checking2/204_coercible_phantom/Main.purs b/tests-integration/fixtures/checking2/204_coercible_phantom/Main.purs new file mode 100644 index 00000000..a0ac3b63 --- /dev/null +++ b/tests-integration/fixtures/checking2/204_coercible_phantom/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Safe.Coerce (coerce) + +data Proxy a = Proxy + +coerceProxy :: forall a b. Proxy a -> Proxy b +coerceProxy = coerce + +coerceProxyIntString :: Proxy Int -> Proxy String +coerceProxyIntString = coerce diff --git a/tests-integration/fixtures/checking2/204_coercible_phantom/Main.snap b/tests-integration/fixtures/checking2/204_coercible_phantom/Main.snap new file mode 100644 index 00000000..d3f49958 --- /dev/null +++ b/tests-integration/fixtures/checking2/204_coercible_phantom/Main.snap @@ -0,0 +1,17 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (t1 :: Type) (a :: (t1 :: Type)). Proxy @(t1 :: Type) (a :: (t1 :: Type)) +coerceProxy :: + forall (t5 :: Type) (t4 :: Type) (a :: (t5 :: Type)) (b :: (t4 :: Type)). + Proxy @(t5 :: Type) (a :: (t5 :: Type)) -> Proxy @(t4 :: Type) (b :: (t4 :: Type)) +coerceProxyIntString :: Proxy @Type Int -> Proxy @Type String + +Types +Proxy :: forall (t1 :: Type). (t1 :: Type) -> Type + +Roles +Proxy = [Phantom] diff --git a/tests-integration/fixtures/checking2/205_coercible_representational/Main.purs b/tests-integration/fixtures/checking2/205_coercible_representational/Main.purs new file mode 100644 index 00000000..3af53be0 --- /dev/null +++ b/tests-integration/fixtures/checking2/205_coercible_representational/Main.purs @@ -0,0 +1,18 @@ +module Main where + +import Safe.Coerce (coerce) + +data Maybe a = Nothing | Just a + +newtype Age = Age Int + +coerceMaybe :: Maybe Age -> Maybe Int +coerceMaybe = coerce + +coerceMaybeReverse :: Maybe Int -> Maybe Age +coerceMaybeReverse = coerce + +newtype UserId = UserId Int + +coerceNested :: Maybe UserId -> Maybe Int +coerceNested = coerce diff --git a/tests-integration/fixtures/checking2/205_coercible_representational/Main.snap b/tests-integration/fixtures/checking2/205_coercible_representational/Main.snap new file mode 100644 index 00000000..ba465de5 --- /dev/null +++ b/tests-integration/fixtures/checking2/205_coercible_representational/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Age :: Int -> Age +coerceMaybe :: Maybe Age -> Maybe Int +coerceMaybeReverse :: Maybe Int -> Maybe Age +UserId :: Int -> UserId +coerceNested :: Maybe UserId -> Maybe Int + +Types +Maybe :: Type -> Type +Age :: Type +UserId :: Type + +Roles +Maybe = [Representational] +Age = [] +UserId = [] diff --git a/tests-integration/fixtures/checking2/206_coercible_array/Main.purs b/tests-integration/fixtures/checking2/206_coercible_array/Main.purs new file mode 100644 index 00000000..290137fa --- /dev/null +++ b/tests-integration/fixtures/checking2/206_coercible_array/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int + +coerceArray :: Array Age -> Array Int +coerceArray = coerce + +coerceArrayReverse :: Array Int -> Array Age +coerceArrayReverse = coerce diff --git a/tests-integration/fixtures/checking2/206_coercible_array/Main.snap b/tests-integration/fixtures/checking2/206_coercible_array/Main.snap new file mode 100644 index 00000000..cfba3bd6 --- /dev/null +++ b/tests-integration/fixtures/checking2/206_coercible_array/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +coerceArray :: Array Age -> Array Int +coerceArrayReverse :: Array Int -> Array Age + +Types +Age :: Type + +Roles +Age = [] diff --git a/tests-integration/fixtures/checking2/207_coercible_record/Main.purs b/tests-integration/fixtures/checking2/207_coercible_record/Main.purs new file mode 100644 index 00000000..42dc5819 --- /dev/null +++ b/tests-integration/fixtures/checking2/207_coercible_record/Main.purs @@ -0,0 +1,16 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int + +coerceRecord :: { name :: String, age :: Age } -> { name :: String, age :: Int } +coerceRecord = coerce + +coerceRecordReverse :: { name :: String, age :: Int } -> { name :: String, age :: Age } +coerceRecordReverse = coerce + +newtype UserId = UserId Int + +coerceMultiple :: { age :: Age, id :: UserId } -> { age :: Int, id :: Int } +coerceMultiple = coerce diff --git a/tests-integration/fixtures/checking2/207_coercible_record/Main.snap b/tests-integration/fixtures/checking2/207_coercible_record/Main.snap new file mode 100644 index 00000000..4b40c09b --- /dev/null +++ b/tests-integration/fixtures/checking2/207_coercible_record/Main.snap @@ -0,0 +1,19 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +coerceRecord :: { age :: Age, name :: String } -> { age :: Int, name :: String } +coerceRecordReverse :: { age :: Int, name :: String } -> { age :: Age, name :: String } +UserId :: Int -> UserId +coerceMultiple :: { age :: Age, id :: UserId } -> { age :: Int, id :: Int } + +Types +Age :: Type +UserId :: Type + +Roles +Age = [] +UserId = [] diff --git a/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.purs b/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.purs new file mode 100644 index 00000000..9cd68596 --- /dev/null +++ b/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.purs @@ -0,0 +1,18 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Age = Age Int +newtype Years = Years Age + +coerceTransitive :: Int -> Years +coerceTransitive = coerce + +unwrapTransitive :: Years -> Int +unwrapTransitive = coerce + +step1 :: Int -> Age +step1 = coerce + +step2 :: Age -> Years +step2 = coerce diff --git a/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.snap b/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.snap new file mode 100644 index 00000000..fc5ad5fc --- /dev/null +++ b/tests-integration/fixtures/checking2/208_coercible_transitivity/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +Years :: Age -> Years +coerceTransitive :: Int -> Years +unwrapTransitive :: Years -> Int +step1 :: Int -> Age +step2 :: Age -> Years + +Types +Age :: Type +Years :: Type + +Roles +Age = [] +Years = [] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 3a0704df..4a391e36 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -433,3 +433,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_202_synonym_forall_expansion_main() { run_test("202_synonym_forall_expansion", "Main"); } #[rustfmt::skip] #[test] fn test_203_binder_instantiation_main() { run_test("203_binder_instantiation", "Main"); } + +#[rustfmt::skip] #[test] fn test_204_coercible_phantom_main() { run_test("204_coercible_phantom", "Main"); } + +#[rustfmt::skip] #[test] fn test_205_coercible_representational_main() { run_test("205_coercible_representational", "Main"); } + +#[rustfmt::skip] #[test] fn test_206_coercible_array_main() { run_test("206_coercible_array", "Main"); } + +#[rustfmt::skip] #[test] fn test_207_coercible_record_main() { run_test("207_coercible_record", "Main"); } + +#[rustfmt::skip] #[test] fn test_208_coercible_transitivity_main() { run_test("208_coercible_transitivity", "Main"); } From 1b925b41bb5ff9ca567462c83c2eed8f8ce65853 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 03:56:09 +0800 Subject: [PATCH 364/386] Add test coverage for coercion failures and constructor visibility --- .../Main.purs | 9 ++++ .../Main.snap | 27 +++++++++++ .../checking2/210_coercible_nominal/Main.purs | 13 ++++++ .../checking2/210_coercible_nominal/Main.snap | 22 +++++++++ .../211_coercible_newtype_hidden/Lib.purs | 3 ++ .../211_coercible_newtype_hidden/Main.purs | 11 +++++ .../211_coercible_newtype_hidden/Main.snap | 46 +++++++++++++++++++ .../212_coercible_newtype_qualified/Lib.purs | 3 ++ .../212_coercible_newtype_qualified/Main.purs | 10 ++++ .../212_coercible_newtype_qualified/Main.snap | 10 ++++ .../Lib.purs | 3 ++ .../Main.purs | 7 +++ .../Main.snap | 28 +++++++++++ .../tests/checking2/generated.rs | 10 ++++ 14 files changed, 202 insertions(+) create mode 100644 tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.snap create mode 100644 tests-integration/fixtures/checking2/210_coercible_nominal/Main.purs create mode 100644 tests-integration/fixtures/checking2/210_coercible_nominal/Main.snap create mode 100644 tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Lib.purs create mode 100644 tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.purs create mode 100644 tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.snap create mode 100644 tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Lib.purs create mode 100644 tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.purs create mode 100644 tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.snap create mode 100644 tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Lib.purs create mode 100644 tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.purs create mode 100644 tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.snap diff --git a/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.purs b/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.purs new file mode 100644 index 00000000..e4945c35 --- /dev/null +++ b/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.purs @@ -0,0 +1,9 @@ +module Main where + +import Safe.Coerce (coerce) + +data Maybe a = Nothing | Just a +data Either a b = Left a | Right b + +coerceDifferent :: Maybe Int -> Either Int String +coerceDifferent = coerce diff --git a/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.snap b/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.snap new file mode 100644 index 00000000..c576118c --- /dev/null +++ b/tests-integration/fixtures/checking2/209_coercible_different_heads_error/Main.snap @@ -0,0 +1,27 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +coerceDifferent :: Maybe Int -> Either Int String + +Types +Maybe :: Type -> Type +Either :: Type -> Type -> Type + +Roles +Maybe = [Representational] +Either = [Representational, Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/210_coercible_nominal/Main.purs b/tests-integration/fixtures/checking2/210_coercible_nominal/Main.purs new file mode 100644 index 00000000..b42e1690 --- /dev/null +++ b/tests-integration/fixtures/checking2/210_coercible_nominal/Main.purs @@ -0,0 +1,13 @@ +module Main where + +import Safe.Coerce (coerce) + +foreign import data Nominal :: Type -> Type + +type role Nominal nominal + +coerceNominalSame :: Nominal Int -> Nominal Int +coerceNominalSame = coerce + +coerceNominalDifferent :: Nominal Int -> Nominal String +coerceNominalDifferent = coerce diff --git a/tests-integration/fixtures/checking2/210_coercible_nominal/Main.snap b/tests-integration/fixtures/checking2/210_coercible_nominal/Main.snap new file mode 100644 index 00000000..3911bcdb --- /dev/null +++ b/tests-integration/fixtures/checking2/210_coercible_nominal/Main.snap @@ -0,0 +1,22 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceNominalSame :: Nominal Int -> Nominal Int +coerceNominalDifferent :: Nominal Int -> Nominal String + +Types +Nominal :: Type -> Type + +Roles +Nominal = [Nominal] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Lib.purs b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Lib.purs new file mode 100644 index 00000000..4eb11d87 --- /dev/null +++ b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Lib.purs @@ -0,0 +1,3 @@ +module Lib (HiddenAge) where + +newtype HiddenAge = HiddenAge Int diff --git a/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.purs b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.purs new file mode 100644 index 00000000..80480037 --- /dev/null +++ b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.purs @@ -0,0 +1,11 @@ +module Main where + +import Lib (HiddenAge) +import Lib as L +import Safe.Coerce (coerce) + +coerceHidden :: Int -> HiddenAge +coerceHidden = coerce + +coerceQualified :: Int -> L.HiddenAge +coerceQualified = coerce diff --git a/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.snap b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.snap new file mode 100644 index 00000000..b2700868 --- /dev/null +++ b/tests-integration/fixtures/checking2/211_coercible_newtype_hidden/Main.snap @@ -0,0 +1,46 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceHidden :: Int -> HiddenAge +coerceQualified :: Int -> HiddenAge + +Types + +Errors +CheckError { + kind: CoercibleConstructorNotInScope { + file_id: Idx::(39), + item_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} +CheckError { + kind: CoercibleConstructorNotInScope { + file_id: Idx::(39), + item_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Lib.purs b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Lib.purs new file mode 100644 index 00000000..a4ad0b02 --- /dev/null +++ b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Lib.purs @@ -0,0 +1,3 @@ +module Lib (Age(..)) where + +newtype Age = Age Int diff --git a/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.purs b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.purs new file mode 100644 index 00000000..7bcc7502 --- /dev/null +++ b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.purs @@ -0,0 +1,10 @@ +module Main where + +import Lib as L +import Safe.Coerce (coerce) + +coerceQualified :: Int -> L.Age +coerceQualified = coerce + +unwrapQualified :: L.Age -> Int +unwrapQualified = coerce diff --git a/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.snap b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.snap new file mode 100644 index 00000000..064b70ba --- /dev/null +++ b/tests-integration/fixtures/checking2/212_coercible_newtype_qualified/Main.snap @@ -0,0 +1,10 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceQualified :: Int -> Age +unwrapQualified :: Age -> Int + +Types diff --git a/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Lib.purs b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Lib.purs new file mode 100644 index 00000000..4eb11d87 --- /dev/null +++ b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Lib.purs @@ -0,0 +1,3 @@ +module Lib (HiddenAge) where + +newtype HiddenAge = HiddenAge Int diff --git a/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.purs b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.purs new file mode 100644 index 00000000..86b390c1 --- /dev/null +++ b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Lib +import Safe.Coerce (coerce) + +coerceOpen :: Int -> HiddenAge +coerceOpen = coerce diff --git a/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.snap b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.snap new file mode 100644 index 00000000..ac0fe0eb --- /dev/null +++ b/tests-integration/fixtures/checking2/213_coercible_newtype_open_hidden/Main.snap @@ -0,0 +1,28 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +coerceOpen :: Int -> HiddenAge + +Types + +Errors +CheckError { + kind: CoercibleConstructorNotInScope { + file_id: Idx::(39), + item_id: Idx::(0), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 4a391e36..90ba0d35 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -443,3 +443,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_207_coercible_record_main() { run_test("207_coercible_record", "Main"); } #[rustfmt::skip] #[test] fn test_208_coercible_transitivity_main() { run_test("208_coercible_transitivity", "Main"); } + +#[rustfmt::skip] #[test] fn test_209_coercible_different_heads_error_main() { run_test("209_coercible_different_heads_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_210_coercible_nominal_main() { run_test("210_coercible_nominal", "Main"); } + +#[rustfmt::skip] #[test] fn test_211_coercible_newtype_hidden_main() { run_test("211_coercible_newtype_hidden", "Main"); } + +#[rustfmt::skip] #[test] fn test_212_coercible_newtype_qualified_main() { run_test("212_coercible_newtype_qualified", "Main"); } + +#[rustfmt::skip] #[test] fn test_213_coercible_newtype_open_hidden_main() { run_test("213_coercible_newtype_open_hidden", "Main"); } From eb8b3e1f8d86e050499dc1bbc12e19916578e8b6 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 03:57:11 +0800 Subject: [PATCH 365/386] Add test coverage for deep and higher-kinded coercions --- .../214_coercible_nested_records/Main.purs | 30 +++++++++++ .../214_coercible_nested_records/Main.snap | 54 +++++++++++++++++++ .../Main.purs | 12 +++++ .../Main.snap | 29 ++++++++++ .../Main.purs | 13 +++++ .../Main.snap | 23 ++++++++ .../Main.purs | 18 +++++++ .../Main.snap | 42 +++++++++++++++ .../Main.purs | 29 ++++++++++ .../Main.snap | 37 +++++++++++++ .../tests/checking2/generated.rs | 10 ++++ 11 files changed, 297 insertions(+) create mode 100644 tests-integration/fixtures/checking2/214_coercible_nested_records/Main.purs create mode 100644 tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap create mode 100644 tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.purs create mode 100644 tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.snap create mode 100644 tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.purs create mode 100644 tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.snap create mode 100644 tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.purs create mode 100644 tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap create mode 100644 tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.purs create mode 100644 tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.snap diff --git a/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.purs b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.purs new file mode 100644 index 00000000..75eec92f --- /dev/null +++ b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.purs @@ -0,0 +1,30 @@ +module Main where + +import Safe.Coerce (coerce) + +newtype Name = Name String +newtype Age = Age Int + +newtype Person = Person { name :: Name, age :: Age } +newtype Company = Company { ceo :: Person, name :: Name } + +type RawPerson = { name :: String, age :: Int } +type RawCompany = { ceo :: RawPerson, name :: String } + +unwrapCompany :: Company -> { ceo :: Person, name :: Name } +unwrapCompany = coerce + +fullyUnwrap :: Company -> RawCompany +fullyUnwrap = coerce + +fullyWrap :: RawCompany -> Company +fullyWrap = coerce + +unwrapPerson :: Person -> RawPerson +unwrapPerson = coerce + +nestedFieldCoerce :: { person :: Person } -> { person :: RawPerson } +nestedFieldCoerce = coerce + +arrayOfRecords :: Array { name :: Name } -> Array { name :: String } +arrayOfRecords = coerce diff --git a/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap new file mode 100644 index 00000000..e1e269e0 --- /dev/null +++ b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap @@ -0,0 +1,54 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Name :: String -> Name +Age :: Int -> Age +Person :: { age :: Age, name :: Name } -> Person +Company :: { ceo :: Person, name :: Name } -> Company +unwrapCompany :: Company -> { ceo :: Person, name :: Name } +fullyUnwrap :: Company -> RawCompany +fullyWrap :: RawCompany -> Company +unwrapPerson :: Person -> RawPerson +nestedFieldCoerce :: { person :: Person } -> { person :: RawPerson } +arrayOfRecords :: Array { name :: Name } -> Array { name :: String } + +Types +Name :: Type +Age :: Type +Person :: Type +Company :: Type +RawPerson :: Type +RawCompany :: Type + +Synonyms +type RawPerson = { age :: Int, name :: String } +type RawCompany = { ceo :: RawPerson, name :: String } + +Roles +Name = [] +Age = [] +Person = [] +Company = [] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(8), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.purs b/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.purs new file mode 100644 index 00000000..9702dacc --- /dev/null +++ b/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.purs @@ -0,0 +1,12 @@ +module Main where + +import Safe.Coerce (coerce) + +data Maybe a = Nothing | Just a +data List a = Nil | Cons a (List a) + +foreign import data Container :: (Type -> Type) -> Type +type role Container representational + +coerceContainerDifferent :: Container Maybe -> Container List +coerceContainerDifferent = coerce diff --git a/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.snap b/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.snap new file mode 100644 index 00000000..f38793c0 --- /dev/null +++ b/tests-integration/fixtures/checking2/215_coercible_higher_kinded_error/Main.snap @@ -0,0 +1,29 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Nothing :: forall (a :: Type). Maybe (a :: Type) +Just :: forall (a :: Type). (a :: Type) -> Maybe (a :: Type) +Nil :: forall (a :: Type). List (a :: Type) +Cons :: forall (a :: Type). (a :: Type) -> List (a :: Type) -> List (a :: Type) +coerceContainerDifferent :: Container Maybe -> Container List + +Types +Maybe :: Type -> Type +List :: Type -> Type +Container :: (Type -> Type) -> Type + +Roles +Maybe = [Representational] +List = [Representational] +Container = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(7), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.purs b/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.purs new file mode 100644 index 00000000..d335a1b3 --- /dev/null +++ b/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.purs @@ -0,0 +1,13 @@ +module Main where + +import Safe.Coerce (coerce) + +data Either a b = Left a | Right b + +newtype EitherAlias a b = EitherAlias (Either a b) + +foreign import data Container :: forall k. k -> Type +type role Container representational + +coerceContainer :: Container Either -> Container EitherAlias +coerceContainer = coerce diff --git a/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.snap b/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.snap new file mode 100644 index 00000000..b98af321 --- /dev/null +++ b/tests-integration/fixtures/checking2/216_coercible_higher_kinded_multi/Main.snap @@ -0,0 +1,23 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Left :: forall (a :: Type) (b :: Type). (a :: Type) -> Either (a :: Type) (b :: Type) +Right :: forall (a :: Type) (b :: Type). (b :: Type) -> Either (a :: Type) (b :: Type) +EitherAlias :: + forall (a :: Type) (b :: Type). + Either (a :: Type) (b :: Type) -> EitherAlias (a :: Type) (b :: Type) +coerceContainer :: + Container @(Type -> Type -> Type) Either -> Container @(Type -> Type -> Type) EitherAlias + +Types +Either :: Type -> Type -> Type +EitherAlias :: Type -> Type -> Type +Container :: forall (k :: Type). (k :: Type) -> Type + +Roles +Either = [Representational, Representational] +EitherAlias = [Representational, Representational] +Container = [Representational] diff --git a/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.purs b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.purs new file mode 100644 index 00000000..4d0535d2 --- /dev/null +++ b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.purs @@ -0,0 +1,18 @@ +module Main where + +import Safe.Coerce (coerce) + +data Maybe :: forall k. k -> Type -> Type +data Maybe n a = Just a | Nothing + +newtype MaybeAlias :: forall k. k -> Type -> Type +newtype MaybeAlias n a = MaybeAlias (Maybe n a) + +foreign import data Container :: (Type -> Type -> Type) -> Type +type role Container representational + +coerceContainer :: Container Maybe -> Container MaybeAlias +coerceContainer = coerce + +coerceContainerReverse :: Container MaybeAlias -> Container Maybe +coerceContainerReverse = coerce diff --git a/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap new file mode 100644 index 00000000..678c8d9b --- /dev/null +++ b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap @@ -0,0 +1,42 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Just :: + forall (k :: Type) (n :: (k :: Type)) (a :: Type). + (a :: Type) -> Maybe @(k :: Type) (n :: (k :: Type)) (a :: Type) +Nothing :: + forall (k :: Type) (n :: (k :: Type)) (a :: Type). + Maybe @(k :: Type) (n :: (k :: Type)) (a :: Type) +MaybeAlias :: + forall (k :: Type) (n :: (k :: Type)) (a :: Type). + Maybe @(k :: Type) (n :: (k :: Type)) (a :: Type) -> + MaybeAlias @(k :: Type) (n :: (k :: Type)) (a :: Type) +coerceContainer :: Container (Maybe @Type) -> Container (MaybeAlias @Type) +coerceContainerReverse :: Container (MaybeAlias @Type) -> Container (Maybe @Type) + +Types +Maybe :: forall (k :: Type). (k :: Type) -> Type -> Type +MaybeAlias :: forall (k :: Type). (k :: Type) -> Type -> Type +Container :: (Type -> Type -> Type) -> Type + +Roles +Maybe = [Phantom, Representational] +MaybeAlias = [Representational, Representational] +Container = [Representational] + +Errors +CheckError { + kind: NoInstanceFound { + constraint: Id(8), + }, + crumbs: [], +} +CheckError { + kind: NoInstanceFound { + constraint: Id(9), + }, + crumbs: [], +} diff --git a/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.purs b/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.purs new file mode 100644 index 00000000..841daa03 --- /dev/null +++ b/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.purs @@ -0,0 +1,29 @@ +module Main where + +import Data.Newtype (class Newtype) +import Safe.Coerce (class Coercible, coerce) + +newtype Age = Age Int + +derive instance Newtype Age _ + +coerceFn :: (Age -> Int) -> (Int -> Age) +coerceFn = coerce + +over :: forall t a s b. Newtype t a => Newtype s b => (a -> t) -> (a -> b) -> t -> s +over _ = coerce + +under :: forall t a s b. Newtype t a => Newtype s b => (a -> t) -> (t -> s) -> a -> b +under _ = coerce + +alaF + :: forall f g t a s b + . Coercible (f t) (f a) + => Coercible (g s) (g b) + => Newtype t a + => Newtype s b + => (a -> t) + -> (f t -> g s) + -> f a + -> g b +alaF _ = coerce diff --git a/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.snap b/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.snap new file mode 100644 index 00000000..b587dbfe --- /dev/null +++ b/tests-integration/fixtures/checking2/218_coercible_function_decomposition/Main.snap @@ -0,0 +1,37 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Age :: Int -> Age +coerceFn :: (Age -> Int) -> Int -> Age +over :: + forall (t :: Type) (a :: Type) (s :: Type) (b :: Type). + Newtype @Type (t :: Type) (a :: Type) => + Newtype @Type (s :: Type) (b :: Type) => + ((a :: Type) -> (t :: Type)) -> ((a :: Type) -> (b :: Type)) -> (t :: Type) -> (s :: Type) +under :: + forall (t :: Type) (a :: Type) (s :: Type) (b :: Type). + Newtype @Type (t :: Type) (a :: Type) => + Newtype @Type (s :: Type) (b :: Type) => + ((a :: Type) -> (t :: Type)) -> ((t :: Type) -> (s :: Type)) -> (a :: Type) -> (b :: Type) +alaF :: + forall (t14 :: Type) (f :: Type -> Type) (g :: (t14 :: Type) -> Type) (t :: Type) (a :: Type) + (s :: (t14 :: Type)) (b :: (t14 :: Type)). + Coercible @Type ((f :: Type -> Type) (t :: Type)) ((f :: Type -> Type) (a :: Type)) => + Coercible @Type + ((g :: (t14 :: Type) -> Type) (s :: (t14 :: Type))) + ((g :: (t14 :: Type) -> Type) (b :: (t14 :: Type))) => + Newtype @Type (t :: Type) (a :: Type) => + Newtype @(t14 :: Type) (s :: (t14 :: Type)) (b :: (t14 :: Type)) => + ((a :: Type) -> (t :: Type)) -> + ((f :: Type -> Type) (t :: Type) -> (g :: (t14 :: Type) -> Type) (s :: (t14 :: Type))) -> + (f :: Type -> Type) (a :: Type) -> + (g :: (t14 :: Type) -> Type) (b :: (t14 :: Type)) + +Types +Age :: Type + +Roles +Age = [] diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 90ba0d35..01cda491 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -453,3 +453,13 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_212_coercible_newtype_qualified_main() { run_test("212_coercible_newtype_qualified", "Main"); } #[rustfmt::skip] #[test] fn test_213_coercible_newtype_open_hidden_main() { run_test("213_coercible_newtype_open_hidden", "Main"); } + +#[rustfmt::skip] #[test] fn test_214_coercible_nested_records_main() { run_test("214_coercible_nested_records", "Main"); } + +#[rustfmt::skip] #[test] fn test_215_coercible_higher_kinded_error_main() { run_test("215_coercible_higher_kinded_error", "Main"); } + +#[rustfmt::skip] #[test] fn test_216_coercible_higher_kinded_multi_main() { run_test("216_coercible_higher_kinded_multi", "Main"); } + +#[rustfmt::skip] #[test] fn test_217_coercible_higher_kinded_polykinded_main() { run_test("217_coercible_higher_kinded_polykinded", "Main"); } + +#[rustfmt::skip] #[test] fn test_218_coercible_function_decomposition_main() { run_test("218_coercible_function_decomposition", "Main"); } From a66f65d549a7c3e3c4d019da247e276d70e3f314 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 04:08:52 +0800 Subject: [PATCH 366/386] Use normalise_expand in Prim.Coerce checks --- .../checking2/src/core/constraint/compiler/prim_coerce.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index dc197def..709b25b2 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -34,8 +34,8 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; + let left = normalise::normalise_expand(state, context, left)?; + let right = normalise::normalise_expand(state, context, right)?; if left == right { return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); @@ -394,8 +394,8 @@ fn try_refl( where Q: ExternalQueries, { - let t1_type = normalise::normalise(state, context, t1_type)?; - let t2_type = normalise::normalise(state, context, t2_type)?; + let t1_type = normalise::normalise_expand(state, context, t1_type)?; + let t2_type = normalise::normalise_expand(state, context, t2_type)?; if t1_type == t2_type { return Ok(true); From c768145b330b1dcf0f9358abeb7369b5c0955f14 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 04:08:52 +0800 Subject: [PATCH 367/386] Extract all applications in newtype extraction --- compiler-core/checking2/src/core/toolkit.rs | 45 ++++++++++++++++--- .../121_derive_newtype_multi_param/Main.snap | 12 ----- .../214_coercible_nested_records/Main.snap | 20 --------- .../Main.snap | 14 ------ 4 files changed, 39 insertions(+), 52 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 44e1a36c..b92507b0 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -10,8 +10,8 @@ use lowering::TypeItemIr; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{ - CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, Role, Type, TypeId, constraint, - normalise, unification, + CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, KindOrType, Role, Type, TypeId, + constraint, normalise, unification, }; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -66,6 +66,35 @@ where Ok((id, arguments)) } +pub fn extract_all_applications( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult<(TypeId, Vec)> +where + Q: ExternalQueries, +{ + let mut arguments = vec![]; + + safe_loop! { + id = normalise::normalise(state, context, id)?; + match context.lookup_type(id) { + Type::Application(function, argument) => { + arguments.push(crate::core::KindOrType::Type(argument)); + id = function; + } + Type::KindApplication(function, argument) => { + arguments.push(crate::core::KindOrType::Kind(argument)); + id = function; + } + _ => break, + } + } + + arguments.reverse(); + Ok((id, arguments)) +} + pub fn lookup_file_type( state: &CheckState, context: &CheckContext, @@ -634,7 +663,7 @@ where let constructor_type = lookup_file_term(state, context, newtype_file, constructor_term_id)?; - let (_, arguments) = extract_type_application(state, context, newtype_type)?; + let (_, arguments) = extract_all_applications(state, context, newtype_type)?; let mut current = constructor_type; let mut arguments = arguments.iter().copied(); @@ -646,9 +675,13 @@ where }; let binder = context.lookup_forall_binder(binder_id); - let replacement = arguments.next().unwrap_or_else(|| { - state.fresh_rigid(context.queries, binder.kind) - }); + let replacement = arguments + .next() + .map(|argument| match argument { + crate::core::KindOrType::Kind(argument) + | crate::core::KindOrType::Type(argument) => argument, + }) + .unwrap_or_else(|| state.fresh_rigid(context.queries, binder.kind)); current = SubstituteName::one(state, context, binder.name, replacement, inner)?; } diff --git a/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap index 1bc11cdc..e9930127 100644 --- a/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap +++ b/tests-integration/fixtures/checking2/121_derive_newtype_multi_param/Main.snap @@ -13,15 +13,3 @@ Pair :: forall (t2 :: Type). Type -> (t2 :: Type) -> Type Roles Pair = [Representational, Phantom] - -Errors -CheckError { - kind: NoInstanceFound { - constraint: Id(8), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap index e1e269e0..49bf8ebf 100644 --- a/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap +++ b/tests-integration/fixtures/checking2/214_coercible_nested_records/Main.snap @@ -32,23 +32,3 @@ Name = [] Age = [] Person = [] Company = [] - -Errors -CheckError { - kind: NoInstanceFound { - constraint: Id(7), - }, - crumbs: [], -} -CheckError { - kind: NoInstanceFound { - constraint: Id(8), - }, - crumbs: [], -} -CheckError { - kind: NoInstanceFound { - constraint: Id(7), - }, - crumbs: [], -} diff --git a/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap index 678c8d9b..f7a3a9db 100644 --- a/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap +++ b/tests-integration/fixtures/checking2/217_coercible_higher_kinded_polykinded/Main.snap @@ -26,17 +26,3 @@ Roles Maybe = [Phantom, Representational] MaybeAlias = [Representational, Representational] Container = [Representational] - -Errors -CheckError { - kind: NoInstanceFound { - constraint: Id(8), - }, - crumbs: [], -} -CheckError { - kind: NoInstanceFound { - constraint: Id(9), - }, - crumbs: [], -} From 9c1a1f5962359b3aa17db3b4975430ff2fa35dd7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 14:16:53 +0800 Subject: [PATCH 368/386] Use normalise_expand and remove redundant normalisation --- .../checking2/src/core/constraint/compiler.rs | 6 ++--- .../core/constraint/compiler/prim_coerce.rs | 16 ++++++------- .../src/core/constraint/compiler/prim_int.rs | 23 +++++-------------- .../constraint/compiler/prim_reflectable.rs | 3 +-- .../src/core/constraint/compiler/prim_row.rs | 17 +------------- .../core/constraint/compiler/prim_row_list.rs | 5 +--- .../core/constraint/compiler/prim_symbol.rs | 14 +---------- .../constraint/compiler/prim_type_error.rs | 8 +++---- 8 files changed, 25 insertions(+), 67 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index 3269c1b3..c992c99b 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -25,7 +25,7 @@ pub fn extract_integer( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Integer(value) => Ok(Some(value)), _ => Ok(None), @@ -40,7 +40,7 @@ pub fn extract_symbol( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; if let Type::String(_, id) = context.lookup_type(id) { Ok(Some(context.queries.lookup_smol_str(id))) } else { @@ -56,7 +56,7 @@ pub fn extract_row( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; if let Type::Row(id) = context.lookup_type(id) { Ok(Some(context.lookup_row_type(id))) } else { diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index 709b25b2..60b8ffaa 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -86,7 +86,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Unification(_) => return Ok(true), Type::Application(function, _) | Type::KindApplication(function, _) => { @@ -106,7 +106,7 @@ where Q: ExternalQueries, { let kind = types::elaborate_kind(state, context, id)?; - let kind = normalise::normalise(state, context, kind)?; + let kind = normalise::normalise_expand(state, context, kind)?; Ok(kind == context.prim.t) } @@ -262,13 +262,13 @@ fn decompose_function_simple( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Function(argument, result) => Ok(Some((argument, result))), Type::Application(partial, result) => { - let partial = normalise::normalise(state, context, partial)?; + let partial = normalise::normalise_expand(state, context, partial)?; if let Type::Application(constructor, argument) = context.lookup_type(partial) { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.function { return Ok(Some((argument, result))); } @@ -288,8 +288,8 @@ fn try_row_coercion( where Q: ExternalQueries, { - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; + let left = normalise::normalise_expand(state, context, left)?; + let right = normalise::normalise_expand(state, context, right)?; let Type::Row(left_row_id) = context.lookup_type(left) else { return Ok(None) }; let Type::Row(right_row_id) = context.lookup_type(right) else { return Ok(None) }; @@ -371,7 +371,7 @@ where Q: ExternalQueries, { safe_loop! { - kind_id = normalise::normalise(state, context, kind_id)?; + kind_id = normalise::normalise_expand(state, context, kind_id)?; match context.lookup_type(kind_id) { Type::Forall(binder_id, inner_kind) => { let binder = context.lookup_forall_binder(binder_id); diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs index 8d623f8b..3a19a55e 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs @@ -24,10 +24,6 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let sum = normalise::normalise(state, context, sum)?; - let left_int = extract_integer(state, context, left)?; let right_int = extract_integer(state, context, right)?; let sum_int = extract_integer(state, context, sum)?; @@ -63,10 +59,6 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let product = normalise::normalise(state, context, product)?; - let Some(left_int) = extract_integer(state, context, left)? else { return Ok(Some(MatchInstance::Stuck)); }; @@ -91,9 +83,9 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let ordering = normalise::normalise(state, context, ordering)?; + let left = normalise::normalise_expand(state, context, left)?; + let right = normalise::normalise_expand(state, context, right)?; + let ordering = normalise::normalise_expand(state, context, ordering)?; let left_int = extract_integer(state, context, left)?; let right_int = extract_integer(state, context, right)?; @@ -136,9 +128,9 @@ where continue; }; - let a = normalise::normalise(state, context, a)?; - let b = normalise::normalise(state, context, b)?; - let relation = normalise::normalise(state, context, relation)?; + let a = normalise::normalise_expand(state, context, a)?; + let b = normalise::normalise_expand(state, context, b)?; + let relation = normalise::normalise_expand(state, context, relation)?; if relation == context.prim_ordering.lt { graph.add_edge(a, b, ()); @@ -175,9 +167,6 @@ where return Ok(None); }; - let int = normalise::normalise(state, context, int)?; - let symbol = normalise::normalise(state, context, symbol)?; - let Some(value) = extract_integer(state, context, int)? else { return Ok(Some(MatchInstance::Stuck)); }; diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs index e084d4d4..141f08ec 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs @@ -19,8 +19,7 @@ where { let &[v, t] = arguments else { return Ok(None) }; - let v = normalise::normalise(state, context, v)?; - let t = normalise::normalise(state, context, t)?; + let v = normalise::normalise_expand(state, context, v)?; if extract_symbol(state, context, v)?.is_some() { return Ok(Some(match_expected(state, context, t, context.prim.string)?)); diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs index c1624c20..0ce90966 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row.rs @@ -8,7 +8,7 @@ use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::constraint::MatchInstance; use crate::core::unification::{CanUnify, can_unify}; -use crate::core::{RowField, RowType, Type, TypeId, normalise}; +use crate::core::{RowField, RowType, Type, TypeId}; use crate::state::CheckState; use super::{extract_row, extract_symbol, match_equality}; @@ -92,10 +92,6 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let union = normalise::normalise(state, context, union)?; - let left_row = extract_row(state, context, left)?; let right_row = extract_row(state, context, right)?; let union_row = extract_row(state, context, union)?; @@ -183,11 +179,6 @@ where return Ok(None); }; - let label = normalise::normalise(state, context, label)?; - let a = normalise::normalise(state, context, a)?; - let tail = normalise::normalise(state, context, tail)?; - let row = normalise::normalise(state, context, row)?; - let label_symbol = extract_symbol(state, context, label)?; let tail_row = extract_row(state, context, tail)?; let row_row = extract_row(state, context, row)?; @@ -239,9 +230,6 @@ where return Ok(None); }; - let label = normalise::normalise(state, context, label)?; - let row = normalise::normalise(state, context, row)?; - let Some(label_value) = extract_symbol(state, context, label)? else { return Ok(Some(MatchInstance::Stuck)); }; @@ -283,9 +271,6 @@ where return Ok(None); }; - let original = normalise::normalise(state, context, original)?; - let nubbed = normalise::normalise(state, context, nubbed)?; - let Some(original_row) = extract_closed_row(state, context, original)? else { return Ok(Some(MatchInstance::Stuck)); }; diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs index fbe5b6a2..17dec521 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs @@ -23,9 +23,6 @@ where return Ok(None); }; - let row = normalise::normalise(state, context, row)?; - let list = normalise::normalise(state, context, list)?; - let Some(row_value) = extract_row(state, context, row)? else { return Ok(Some(MatchInstance::Stuck)); }; @@ -66,7 +63,7 @@ where let singleton_row_type = context.intern_row(singleton_row_id); let row_kind = types::elaborate_kind(state, context, singleton_row_type)?; - let row_kind = normalise::normalise(state, context, row_kind)?; + let row_kind = normalise::normalise_expand(state, context, row_kind)?; let Type::Application(_, element_kind) = context.lookup_type(row_kind) else { return Ok(state.fresh_unification(context.queries, context.prim.t)); }; diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs index f17848bf..0bd089aa 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs @@ -22,10 +22,6 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let appended = normalise::normalise(state, context, appended)?; - let left_symbol = extract_symbol(state, context, left)?; let right_symbol = extract_symbol(state, context, right)?; let appended_symbol = extract_symbol(state, context, appended)?; @@ -69,10 +65,6 @@ where return Ok(None); }; - let left = normalise::normalise(state, context, left)?; - let right = normalise::normalise(state, context, right)?; - let ordering = normalise::normalise(state, context, ordering)?; - let Some(left_symbol) = extract_symbol(state, context, left)? else { return Ok(Some(MatchInstance::Stuck)); }; @@ -101,10 +93,6 @@ where return Ok(None); }; - let head = normalise::normalise(state, context, head)?; - let tail = normalise::normalise(state, context, tail)?; - let symbol = normalise::normalise(state, context, symbol)?; - let head_symbol = extract_symbol(state, context, head)?; let tail_symbol = extract_symbol(state, context, tail)?; let symbol_symbol = extract_symbol(state, context, symbol)?; @@ -157,7 +145,7 @@ where return Ok(None); }; - let symbol = normalise::normalise(state, context, symbol)?; + let symbol = normalise::normalise_expand(state, context, symbol)?; let matched = if extract_symbol(state, context, symbol)?.is_some() { MatchInstance::Match { constraints: vec![], equalities: vec![] } diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs index fef66c2b..a916fe5b 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs @@ -14,7 +14,7 @@ fn is_stuck(state: &mut CheckState, context: &CheckContext, id: TypeId) -> where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; Ok(matches!(context.lookup_type(id), Type::Unification(_))) } @@ -26,7 +26,7 @@ fn extract_symbol_text( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; if let Type::String(_, smol_str_id) = context.lookup_type(id) { Ok(Some(context.queries.lookup_smol_str(smol_str_id))) } else { @@ -42,7 +42,7 @@ fn extract_symbol_with_kind( where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::normalise_expand(state, context, id)?; if let Type::String(kind, smol_id) = context.lookup_type(id) { Ok(Some((kind, context.queries.lookup_smol_str(smol_id)))) } else { @@ -82,7 +82,7 @@ fn render_doc( where Q: ExternalQueries, { - let doc = normalise::normalise(state, context, doc)?; + let doc = normalise::normalise_expand(state, context, doc)?; if matches!(context.lookup_type(doc), Type::Unification(_)) { return Ok(None); From a8cfbaef2242940001fe8e8b1b594bb9589edbc0 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 14:32:15 +0800 Subject: [PATCH 369/386] Add NonLocalNewtype and coverage for error cases --- compiler-core/checking2/src/core/toolkit.rs | 4 ++ compiler-core/checking2/src/error.rs | 3 ++ .../checking2/src/source/derive/newtype.rs | 8 ++-- .../Main.purs | 5 +++ .../Main.snap | 20 ++++++++++ .../Main.purs | 5 +++ .../Main.snap | 20 ++++++++++ .../222_derive_newtype_not_local/Lib.purs | 3 ++ .../222_derive_newtype_not_local/Main.purs | 6 +++ .../222_derive_newtype_not_local/Main.snap | 20 ++++++++++ .../Lib.purs | 3 ++ .../Main.purs | 6 +++ .../Main.snap | 20 ++++++++++ .../Main.purs | 5 +++ .../Main.snap | 34 ++++++++++++++++ .../Main.purs | 7 ++++ .../Main.snap | 39 +++++++++++++++++++ .../tests/checking2/generated.rs | 12 ++++++ 18 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.snap create mode 100644 tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.purs create mode 100644 tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.snap create mode 100644 tests-integration/fixtures/checking2/222_derive_newtype_not_local/Lib.purs create mode 100644 tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.purs create mode 100644 tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.snap create mode 100644 tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Lib.purs create mode 100644 tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.purs create mode 100644 tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.snap create mode 100644 tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.purs create mode 100644 tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.snap create mode 100644 tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.purs create mode 100644 tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.snap diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index b92507b0..9c954196 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -650,6 +650,10 @@ pub fn get_newtype_inner( where Q: ExternalQueries, { + if !is_newtype(context, newtype_file, newtype_id)? { + return Ok(None); + } + let constructor_term_id = if newtype_file == context.id { context.indexed.pairs.data_constructors(newtype_id).next() } else { diff --git a/compiler-core/checking2/src/error.rs b/compiler-core/checking2/src/error.rs index 39f4a302..25db6fb5 100644 --- a/compiler-core/checking2/src/error.rs +++ b/compiler-core/checking2/src/error.rs @@ -97,6 +97,9 @@ pub enum ErrorKind { type_message: SmolStrId, }, InvalidNewtypeDeriveSkolemArguments, + NonLocalNewtype { + type_message: SmolStrId, + }, NoInstanceFound { constraint: SmolStrId, }, diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs index a2c4b2a4..6603d159 100644 --- a/compiler-core/checking2/src/source/derive/newtype.rs +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -32,9 +32,9 @@ where return Ok(None); }; - if newtype_file != context.id || !toolkit::is_newtype(context, newtype_file, newtype_id)? { + if newtype_file != context.id { let type_message = state.pretty_id(context, *newtype_type)?; - state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + state.insert_error(ErrorKind::NonLocalNewtype { type_message }); return Ok(None); } @@ -78,9 +78,9 @@ where return Ok(None); }; - if newtype_file != context.id || !toolkit::is_newtype(context, newtype_file, newtype_id)? { + if newtype_file != context.id { let type_message = state.pretty_id(context, *newtype_type)?; - state.insert_error(ErrorKind::ExpectedNewtype { type_message }); + state.insert_error(ErrorKind::NonLocalNewtype { type_message }); return Ok(None); } diff --git a/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.purs b/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.purs new file mode 100644 index 00000000..801d8b76 --- /dev/null +++ b/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Data.Show (class Show) + +derive newtype instance Show (Int -> Int) diff --git a/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.snap b/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.snap new file mode 100644 index 00000000..75c7ab0f --- /dev/null +++ b/tests-integration/fixtures/checking2/219_derive_newtype_not_constructor/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: CannotDeriveForType { + type_message: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.purs b/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.purs new file mode 100644 index 00000000..eb2ece06 --- /dev/null +++ b/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Data.Newtype (class Newtype) + +derive instance Newtype (Int -> Int) _ diff --git a/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.snap b/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.snap new file mode 100644 index 00000000..ae3feb6c --- /dev/null +++ b/tests-integration/fixtures/checking2/220_derive_newtype_class_not_constructor/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: CannotDeriveForType { + type_message: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Lib.purs b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Lib.purs new file mode 100644 index 00000000..07b9c606 --- /dev/null +++ b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Lib.purs @@ -0,0 +1,3 @@ +module Lib where + +newtype Wrapper = Wrapper Int diff --git a/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.purs b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.purs new file mode 100644 index 00000000..ec294c25 --- /dev/null +++ b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.purs @@ -0,0 +1,6 @@ +module Main where + +import Data.Show (class Show) +import Lib (Wrapper) + +derive newtype instance Show Wrapper diff --git a/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.snap b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.snap new file mode 100644 index 00000000..4e8b0a19 --- /dev/null +++ b/tests-integration/fixtures/checking2/222_derive_newtype_not_local/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: NonLocalNewtype { + type_message: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Lib.purs b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Lib.purs new file mode 100644 index 00000000..07b9c606 --- /dev/null +++ b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Lib.purs @@ -0,0 +1,3 @@ +module Lib where + +newtype Wrapper = Wrapper Int diff --git a/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.purs b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.purs new file mode 100644 index 00000000..d5229377 --- /dev/null +++ b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.purs @@ -0,0 +1,6 @@ +module Main where + +import Data.Newtype (class Newtype) +import Lib (Wrapper) + +derive instance Newtype Wrapper _ diff --git a/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.snap b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.snap new file mode 100644 index 00000000..bdca8411 --- /dev/null +++ b/tests-integration/fixtures/checking2/223_derive_newtype_class_not_local/Main.snap @@ -0,0 +1,20 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: NonLocalNewtype { + type_message: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.purs b/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.purs new file mode 100644 index 00000000..a1574961 --- /dev/null +++ b/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.purs @@ -0,0 +1,5 @@ +module Main where + +import Data.Show (class Show) + +derive newtype instance Show diff --git a/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.snap new file mode 100644 index 00000000..82bb73dd --- /dev/null +++ b/tests-integration/fixtures/checking2/224_derive_newtype_insufficient_params/Main.snap @@ -0,0 +1,34 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types + +Errors +CheckError { + kind: DeriveInvalidArity { + class_file: Idx::(30), + class_id: Idx::(0), + expected: 1, + actual: 0, + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(7), + t2: Id(8), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.purs b/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.purs new file mode 100644 index 00000000..99e44ce1 --- /dev/null +++ b/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.purs @@ -0,0 +1,7 @@ +module Main where + +import Data.Newtype (class Newtype) + +newtype Wrapper = Wrapper Int + +derive instance Newtype Wrapper diff --git a/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.snap b/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.snap new file mode 100644 index 00000000..e02cdf7f --- /dev/null +++ b/tests-integration/fixtures/checking2/225_derive_newtype_class_insufficient_params/Main.snap @@ -0,0 +1,39 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Wrapper :: Int -> Wrapper + +Types +Wrapper :: Type + +Roles +Wrapper = [] + +Errors +CheckError { + kind: DeriveInvalidArity { + class_file: Idx::(24), + class_id: Idx::(0), + expected: 2, + actual: 1, + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(1), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 01cda491..049419e3 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -463,3 +463,15 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_217_coercible_higher_kinded_polykinded_main() { run_test("217_coercible_higher_kinded_polykinded", "Main"); } #[rustfmt::skip] #[test] fn test_218_coercible_function_decomposition_main() { run_test("218_coercible_function_decomposition", "Main"); } + +#[rustfmt::skip] #[test] fn test_219_derive_newtype_not_constructor_main() { run_test("219_derive_newtype_not_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_220_derive_newtype_class_not_constructor_main() { run_test("220_derive_newtype_class_not_constructor", "Main"); } + +#[rustfmt::skip] #[test] fn test_222_derive_newtype_not_local_main() { run_test("222_derive_newtype_not_local", "Main"); } + +#[rustfmt::skip] #[test] fn test_223_derive_newtype_class_not_local_main() { run_test("223_derive_newtype_class_not_local", "Main"); } + +#[rustfmt::skip] #[test] fn test_224_derive_newtype_insufficient_params_main() { run_test("224_derive_newtype_insufficient_params", "Main"); } + +#[rustfmt::skip] #[test] fn test_225_derive_newtype_class_insufficient_params_main() { run_test("225_derive_newtype_class_insufficient_params", "Main"); } From 9cd0f95bb3a6c9a1242db3896a9f548af00c28dc Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 14:54:46 +0800 Subject: [PATCH 370/386] Use normalise_expand in more places --- .../checking2/src/core/constraint.rs | 2 +- .../checking2/src/core/constraint/given.rs | 8 +++--- .../src/core/constraint/instances.rs | 4 +-- .../checking2/src/core/exhaustive.rs | 4 +-- .../checking2/src/core/exhaustive/convert.rs | 6 ++--- .../checking2/src/core/generalise.rs | 2 +- compiler-core/checking2/src/core/toolkit.rs | 8 +++--- .../checking2/src/core/unification.rs | 4 +-- compiler-core/checking2/src/source/binder.rs | 26 +++++++++++-------- .../checking2/src/source/derive/field.rs | 8 +++--- .../checking2/src/source/derive/generic.rs | 4 +-- .../checking2/src/source/derive/variance.rs | 6 ++--- compiler-core/checking2/src/source/roles.rs | 5 ++-- .../checking2/src/source/terms/application.rs | 8 +++--- .../checking2/src/source/terms/collections.rs | 10 +++---- .../202_synonym_forall_expansion/Main.snap | 16 ------------ 16 files changed, 55 insertions(+), 66 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index c56ccb35..cd7bbf3d 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -190,7 +190,7 @@ where Q: ExternalQueries, { let (constructor, arguments) = toolkit::extract_type_application(state, context, id)?; - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; Ok(match context.lookup_type(constructor) { Type::Constructor(file_id, item_id) => { Some(ConstraintApplication { file_id, item_id, arguments }) diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index 30d17e54..5fd64a71 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -121,8 +121,8 @@ fn match_given_type( where Q: ExternalQueries, { - let wanted = normalise::normalise(state, context, wanted)?; - let given = normalise::normalise(state, context, given)?; + let wanted = normalise::normalise_expand(state, context, wanted)?; + let given = normalise::normalise_expand(state, context, given)?; if wanted == given { return Ok(MatchType::Match); @@ -189,7 +189,7 @@ where result.and_then(|| match_given_type(state, context, wanted_tail, given_tail)) } (Some(wanted_tail), None) => { - let wanted_tail = normalise::normalise(state, context, wanted_tail)?; + let wanted_tail = normalise::normalise_expand(state, context, wanted_tail)?; if matches!(context.lookup_type(wanted_tail), Type::Unification(_)) { Ok(MatchType::Stuck) } else { @@ -197,7 +197,7 @@ where } } (None, Some(given_tail)) => { - let given_tail = normalise::normalise(state, context, given_tail)?; + let given_tail = normalise::normalise_expand(state, context, given_tail)?; if matches!(context.lookup_type(given_tail), Type::Unification(_)) { Ok(MatchType::Stuck) } else { diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index cf5a3a76..a4b6cad8 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -281,8 +281,8 @@ fn match_type( where Q: ExternalQueries, { - let wanted = normalise::normalise(state, context, wanted)?; - let given = normalise::normalise(state, context, given)?; + let wanted = normalise::normalise_expand(state, context, wanted)?; + let given = normalise::normalise_expand(state, context, given)?; if wanted == given { return Ok(MatchType::Match); diff --git a/compiler-core/checking2/src/core/exhaustive.rs b/compiler-core/checking2/src/core/exhaustive.rs index cc016c5b..62ff3145 100644 --- a/compiler-core/checking2/src/core/exhaustive.rs +++ b/compiler-core/checking2/src/core/exhaustive.rs @@ -999,7 +999,7 @@ where let mut current_id = applied_type; safe_loop! { - current_id = normalise::normalise(state, context, current_id)?; + current_id = normalise::normalise_expand(state, context, current_id)?; match context.lookup_type(current_id) { Type::Application(function, argument) => { arguments.push(argument); @@ -1033,7 +1033,7 @@ where let mut arguments_iter = arguments.as_ref().iter().copied(); safe_loop! { - type_id = normalise::normalise(state, context, type_id)?; + type_id = normalise::normalise_expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); diff --git a/compiler-core/checking2/src/core/exhaustive/convert.rs b/compiler-core/checking2/src/core/exhaustive/convert.rs index be1dd394..b5bde6ad 100644 --- a/compiler-core/checking2/src/core/exhaustive/convert.rs +++ b/compiler-core/checking2/src/core/exhaustive/convert.rs @@ -152,7 +152,7 @@ where return Ok(None); }; - let row_type_id = normalise::normalise(state, context, *row_type_id)?; + let row_type_id = normalise::normalise_expand(state, context, *row_type_id)?; let row_fields = if let Type::Row(row_type_id) = context.lookup_type(row_type_id) { context.lookup_row_type(row_type_id).fields @@ -353,7 +353,7 @@ where let mut arguments = vec![]; safe_loop! { - type_id = normalise::normalise(state, context, type_id)?; + type_id = normalise::normalise_expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Application(function, argument) => { arguments.push(argument); @@ -378,5 +378,5 @@ fn normalise_expand_type( where Q: ExternalQueries, { - normalise::normalise(state, context, type_id) + normalise::normalise_expand(state, context, type_id) } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index 4cd4b3b1..f882392a 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -220,7 +220,7 @@ where (name, kind) } UnificationState::Solved(solution) => { - let solution = normalise::normalise(state, context, solution)?; + let solution = normalise::normalise_expand(state, context, solution)?; let Type::Rigid(name, _, kind) = context.lookup_type(solution) else { continue; }; diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 9c954196..fda57fcb 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -49,7 +49,7 @@ where let mut arguments = vec![]; safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Application(function, argument) => { arguments.push(argument); @@ -77,7 +77,7 @@ where let mut arguments = vec![]; safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Application(function, argument) => { arguments.push(crate::core::KindOrType::Type(argument)); @@ -607,7 +607,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Constructor(file_id, item_id) => return Ok(Some((file_id, item_id))), Type::Application(function, _) | Type::KindApplication(function, _) => { @@ -673,7 +673,7 @@ where let mut arguments = arguments.iter().copied(); safe_loop! { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index c4745b33..43386b59 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -207,8 +207,8 @@ where Type::Application(t1_function, t1_argument), Type::Application(t2_function, t2_argument), ) if t1_function == context.prim.record && t2_function == context.prim.record => { - let t1_argument = normalise::normalise(state, context, t1_argument)?; - let t2_argument = normalise::normalise(state, context, t2_argument)?; + let t1_argument = normalise::normalise_expand(state, context, t1_argument)?; + let t2_argument = normalise::normalise_expand(state, context, t2_argument)?; let t1_argument_core = context.queries.lookup_type(t1_argument); let t2_argument_core = context.queries.lookup_type(t2_argument); diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs index e2ad28c7..9297bf93 100644 --- a/compiler-core/checking2/src/source/binder.rs +++ b/compiler-core/checking2/src/source/binder.rs @@ -314,7 +314,7 @@ where Q: ExternalQueries, { loop { - id = normalise::normalise(state, context, id)?; + id = normalise::normalise_expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_wanted(constraint); @@ -337,7 +337,7 @@ where Q: ExternalQueries, F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { - let function_t = normalise::normalise(state, context, function_t)?; + let function_t = normalise::normalise_expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Function(argument_type, result_type) => { @@ -377,10 +377,10 @@ where } Type::Application(partial, result_type) => { - let partial = normalise::normalise(state, context, partial)?; + let partial = normalise::normalise_expand(state, context, partial)?; match context.lookup_type(partial) { Type::Application(constructor, argument_type) => { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.function { check_argument(state, context, argument_id, argument_type)?; return Ok(result_type); @@ -520,14 +520,15 @@ fn extract_expected_row( where Q: ExternalQueries, { - let expected_type = normalise::normalise(state, context, expected_type)?; + let expected_type = normalise::normalise_expand(state, context, expected_type)?; let Type::Application(function, argument) = context.lookup_type(expected_type) else { return Ok(None); }; + let function = normalise::normalise_expand(state, context, function)?; if function != context.prim.record { return Ok(None); } - let row = normalise::normalise(state, context, argument)?; + let row = normalise::normalise_expand(state, context, argument)?; let Type::Row(row_id) = context.lookup_type(row) else { return Ok(None); }; @@ -547,12 +548,15 @@ where { let pattern_items = collect_pattern_items(record); - let expected_type = normalise::normalise(state, context, expected_type)?; + let expected_type = normalise::normalise_expand(state, context, expected_type)?; - let expected_row = if let Type::Application(function, _) = context.lookup_type(expected_type) - && function == context.prim.record - { - extract_expected_row(state, context, expected_type)? + let expected_row = if let Type::Application(function, _) = context.lookup_type(expected_type) { + let function = normalise::normalise_expand(state, context, function)?; + if function == context.prim.record { + extract_expected_row(state, context, expected_type)? + } else { + None + } } else { None }; diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs index dcd95a4b..63efcd48 100644 --- a/compiler-core/checking2/src/source/derive/field.rs +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -56,7 +56,7 @@ where let mut arguments = arguments.iter().copied(); loop { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; @@ -82,11 +82,11 @@ fn generate_constraint( where Q: ExternalQueries, { - let type_id = normalise::normalise(state, context, type_id)?; + let type_id = normalise::normalise_expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Application(function, argument) => { - let function = normalise::normalise(state, context, function)?; + let function = normalise::normalise_expand(state, context, function)?; if function == context.prim.record { generate_constraint(state, context, argument, class, class1)?; } else if is_type_to_type_variable(state, context, function)? { @@ -121,7 +121,7 @@ fn is_type_to_type_variable( where Q: ExternalQueries, { - let type_id = normalise::normalise(state, context, type_id)?; + let type_id = normalise::normalise_expand(state, context, type_id)?; let kind = match context.lookup_type(type_id) { Type::Rigid(_, _, kind) => kind, Type::Unification(unification_id) => state.unifications.get(unification_id).kind, diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs index 8f686d09..a25c3281 100644 --- a/compiler-core/checking2/src/source/derive/generic.rs +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -150,7 +150,7 @@ where { let mut arguments = vec![]; safe_loop! { - applied_type = normalise::normalise(state, context, applied_type)?; + applied_type = normalise::normalise_expand(state, context, applied_type)?; match context.lookup_type(applied_type) { Type::Application(function, argument) | Type::KindApplication(function, argument) => { arguments.push(argument); @@ -176,7 +176,7 @@ where let mut arguments = arguments.iter().copied(); safe_loop! { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 24041680..63613b43 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -101,7 +101,7 @@ where let mut names = vec![]; loop { - current = normalise::normalise(state, context, current)?; + current = normalise::normalise_expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; @@ -153,7 +153,7 @@ fn check_variance_field( where Q: ExternalQueries, { - let type_id = normalise::normalise(state, context, type_id)?; + let type_id = normalise::normalise_expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Rigid(name, _, _) => { @@ -166,7 +166,7 @@ where check_variance_field(state, context, result, variance, rigids)?; } Type::Application(function, argument) => { - let function = normalise::normalise(state, context, function)?; + let function = normalise::normalise_expand(state, context, function)?; if function == context.prim.record { check_variance_field(state, context, argument, variance, rigids)?; } else { diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index 00e50bfb..5c7b1c1c 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -161,7 +161,7 @@ fn infer_roles<'a, 'q, Q>( where Q: ExternalQueries, { - let type_id = normalise::normalise(inference.state, inference.context, type_id)?; + let type_id = normalise::normalise_expand(inference.state, inference.context, type_id)?; match inference.context.lookup_type(type_id) { Type::Rigid(name, _, kind) => { @@ -178,7 +178,8 @@ where } Type::Application(function, argument) => { - let function_id = normalise::normalise(inference.state, inference.context, function)?; + let function_id = + normalise::normalise_expand(inference.state, inference.context, function)?; let is_type_variable = matches!(inference.context.lookup_type(function_id), Type::Rigid(_, _, _)); diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs index 29f2b079..a169bf91 100644 --- a/compiler-core/checking2/src/source/terms/application.rs +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -52,7 +52,7 @@ where Q: ExternalQueries, F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { - let function_t = normalise::normalise(state, context, function_t)?; + let function_t = normalise::normalise_expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Function(argument_type, result_type) => { @@ -92,10 +92,10 @@ where } Type::Application(partial, result_type) => { - let partial = normalise::normalise(state, context, partial)?; + let partial = normalise::normalise_expand(state, context, partial)?; match context.lookup_type(partial) { Type::Application(constructor, argument_type) => { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.function { check_argument(state, context, argument_id, argument_type)?; return Ok(result_type); @@ -173,7 +173,7 @@ pub fn check_function_type_application( where Q: ExternalQueries, { - let function_t = normalise::normalise(state, context, function_t)?; + let function_t = normalise::normalise_expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); diff --git a/compiler-core/checking2/src/source/terms/collections.rs b/compiler-core/checking2/src/source/terms/collections.rs index ff1f8c78..70325e91 100644 --- a/compiler-core/checking2/src/source/terms/collections.rs +++ b/compiler-core/checking2/src/source/terms/collections.rs @@ -35,9 +35,9 @@ pub fn check_array( where Q: ExternalQueries, { - let normalised = normalise::normalise(state, context, expected)?; + let normalised = normalise::normalise_expand(state, context, expected)?; if let Type::Application(constructor, element_type) = context.lookup_type(normalised) { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.array { for expression in array.iter() { super::check_expression(state, context, *expression, element_type)?; @@ -105,11 +105,11 @@ pub fn check_record( where Q: ExternalQueries, { - let normalised = normalise::normalise(state, context, expected)?; + let normalised = normalise::normalise_expand(state, context, expected)?; if let Type::Application(constructor, row_type) = context.lookup_type(normalised) { - let constructor = normalise::normalise(state, context, constructor)?; + let constructor = normalise::normalise_expand(state, context, constructor)?; if constructor == context.prim.record { - let row_type = normalise::normalise(state, context, row_type)?; + let row_type = normalise::normalise_expand(state, context, row_type)?; if let Type::Row(row_id) = context.lookup_type(row_type) { let expected_fields = context.lookup_row_type(row_id); diff --git a/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap index 85dcf2df..4386d09f 100644 --- a/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap +++ b/tests-integration/fixtures/checking2/202_synonym_forall_expansion/Main.snap @@ -16,19 +16,3 @@ NatTrans :: forall (t3 :: Type). ((t3 :: Type) -> Type) -> ((t3 :: Type) -> Type Synonyms type NatTrans f g = forall (a :: (t3 :: Type)). (f :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) -> (g :: (t3 :: Type) -> Type) (a :: (t3 :: Type)) - -Errors -CheckError { - kind: CannotUnify { - t1: Id(9), - t2: Id(10), - }, - crumbs: [ - TermDeclaration( - Idx::(0), - ), - CheckingExpression( - AstId(39), - ), - ], -} From 335cada6d8a4ec3a7add5a85554c026a8b8c72f2 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 16:55:18 +0800 Subject: [PATCH 371/386] Rename normalise_expand to expand --- .../checking2/src/core/constraint.rs | 2 +- .../checking2/src/core/constraint/compiler.rs | 6 ++-- .../core/constraint/compiler/prim_coerce.rs | 24 ++++++------- .../src/core/constraint/compiler/prim_int.rs | 12 +++---- .../constraint/compiler/prim_reflectable.rs | 2 +- .../core/constraint/compiler/prim_row_list.rs | 2 +- .../core/constraint/compiler/prim_symbol.rs | 2 +- .../constraint/compiler/prim_type_error.rs | 8 ++--- .../checking2/src/core/constraint/given.rs | 8 ++--- .../src/core/constraint/instances.rs | 4 +-- .../checking2/src/core/exhaustive.rs | 4 +-- .../checking2/src/core/exhaustive/convert.rs | 6 ++-- .../checking2/src/core/generalise.rs | 2 +- compiler-core/checking2/src/core/normalise.rs | 36 +++++++++---------- compiler-core/checking2/src/core/signature.rs | 6 ++-- compiler-core/checking2/src/core/toolkit.rs | 34 +++++++++--------- .../checking2/src/core/unification.rs | 16 ++++----- compiler-core/checking2/src/source/binder.rs | 18 +++++----- .../checking2/src/source/derive/field.rs | 8 ++--- .../checking2/src/source/derive/generic.rs | 4 +-- .../checking2/src/source/derive/variance.rs | 6 ++-- compiler-core/checking2/src/source/roles.rs | 4 +-- .../checking2/src/source/terms/application.rs | 8 ++--- .../checking2/src/source/terms/collections.rs | 10 +++--- 24 files changed, 116 insertions(+), 116 deletions(-) diff --git a/compiler-core/checking2/src/core/constraint.rs b/compiler-core/checking2/src/core/constraint.rs index cd7bbf3d..e01cd975 100644 --- a/compiler-core/checking2/src/core/constraint.rs +++ b/compiler-core/checking2/src/core/constraint.rs @@ -190,7 +190,7 @@ where Q: ExternalQueries, { let (constructor, arguments) = toolkit::extract_type_application(state, context, id)?; - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; Ok(match context.lookup_type(constructor) { Type::Constructor(file_id, item_id) => { Some(ConstraintApplication { file_id, item_id, arguments }) diff --git a/compiler-core/checking2/src/core/constraint/compiler.rs b/compiler-core/checking2/src/core/constraint/compiler.rs index c992c99b..ea27952c 100644 --- a/compiler-core/checking2/src/core/constraint/compiler.rs +++ b/compiler-core/checking2/src/core/constraint/compiler.rs @@ -25,7 +25,7 @@ pub fn extract_integer( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Integer(value) => Ok(Some(value)), _ => Ok(None), @@ -40,7 +40,7 @@ pub fn extract_symbol( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; if let Type::String(_, id) = context.lookup_type(id) { Ok(Some(context.queries.lookup_smol_str(id))) } else { @@ -56,7 +56,7 @@ pub fn extract_row( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; if let Type::Row(id) = context.lookup_type(id) { Ok(Some(context.lookup_row_type(id))) } else { diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index 60b8ffaa..b4976957 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -34,8 +34,8 @@ where return Ok(None); }; - let left = normalise::normalise_expand(state, context, left)?; - let right = normalise::normalise_expand(state, context, right)?; + let left = normalise::expand(state, context, left)?; + let right = normalise::expand(state, context, right)?; if left == right { return Ok(Some(MatchInstance::Match { constraints: vec![], equalities: vec![] })); @@ -86,7 +86,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Unification(_) => return Ok(true), Type::Application(function, _) | Type::KindApplication(function, _) => { @@ -106,7 +106,7 @@ where Q: ExternalQueries, { let kind = types::elaborate_kind(state, context, id)?; - let kind = normalise::normalise_expand(state, context, kind)?; + let kind = normalise::expand(state, context, kind)?; Ok(kind == context.prim.t) } @@ -262,13 +262,13 @@ fn decompose_function_simple( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Function(argument, result) => Ok(Some((argument, result))), Type::Application(partial, result) => { - let partial = normalise::normalise_expand(state, context, partial)?; + let partial = normalise::expand(state, context, partial)?; if let Type::Application(constructor, argument) = context.lookup_type(partial) { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.function { return Ok(Some((argument, result))); } @@ -288,8 +288,8 @@ fn try_row_coercion( where Q: ExternalQueries, { - let left = normalise::normalise_expand(state, context, left)?; - let right = normalise::normalise_expand(state, context, right)?; + let left = normalise::expand(state, context, left)?; + let right = normalise::expand(state, context, right)?; let Type::Row(left_row_id) = context.lookup_type(left) else { return Ok(None) }; let Type::Row(right_row_id) = context.lookup_type(right) else { return Ok(None) }; @@ -371,7 +371,7 @@ where Q: ExternalQueries, { safe_loop! { - kind_id = normalise::normalise_expand(state, context, kind_id)?; + kind_id = normalise::expand(state, context, kind_id)?; match context.lookup_type(kind_id) { Type::Forall(binder_id, inner_kind) => { let binder = context.lookup_forall_binder(binder_id); @@ -394,8 +394,8 @@ fn try_refl( where Q: ExternalQueries, { - let t1_type = normalise::normalise_expand(state, context, t1_type)?; - let t2_type = normalise::normalise_expand(state, context, t2_type)?; + let t1_type = normalise::expand(state, context, t1_type)?; + let t2_type = normalise::expand(state, context, t2_type)?; if t1_type == t2_type { return Ok(true); diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs index 3a19a55e..e1bc48c5 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_int.rs @@ -83,9 +83,9 @@ where return Ok(None); }; - let left = normalise::normalise_expand(state, context, left)?; - let right = normalise::normalise_expand(state, context, right)?; - let ordering = normalise::normalise_expand(state, context, ordering)?; + let left = normalise::expand(state, context, left)?; + let right = normalise::expand(state, context, right)?; + let ordering = normalise::expand(state, context, ordering)?; let left_int = extract_integer(state, context, left)?; let right_int = extract_integer(state, context, right)?; @@ -128,9 +128,9 @@ where continue; }; - let a = normalise::normalise_expand(state, context, a)?; - let b = normalise::normalise_expand(state, context, b)?; - let relation = normalise::normalise_expand(state, context, relation)?; + let a = normalise::expand(state, context, a)?; + let b = normalise::expand(state, context, b)?; + let relation = normalise::expand(state, context, relation)?; if relation == context.prim_ordering.lt { graph.add_edge(a, b, ()); diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs index 141f08ec..4ac68097 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_reflectable.rs @@ -19,7 +19,7 @@ where { let &[v, t] = arguments else { return Ok(None) }; - let v = normalise::normalise_expand(state, context, v)?; + let v = normalise::expand(state, context, v)?; if extract_symbol(state, context, v)?.is_some() { return Ok(Some(match_expected(state, context, t, context.prim.string)?)); diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs index 17dec521..3a780f21 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_row_list.rs @@ -63,7 +63,7 @@ where let singleton_row_type = context.intern_row(singleton_row_id); let row_kind = types::elaborate_kind(state, context, singleton_row_type)?; - let row_kind = normalise::normalise_expand(state, context, row_kind)?; + let row_kind = normalise::expand(state, context, row_kind)?; let Type::Application(_, element_kind) = context.lookup_type(row_kind) else { return Ok(state.fresh_unification(context.queries, context.prim.t)); }; diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs index 0bd089aa..90e6d0ef 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_symbol.rs @@ -145,7 +145,7 @@ where return Ok(None); }; - let symbol = normalise::normalise_expand(state, context, symbol)?; + let symbol = normalise::expand(state, context, symbol)?; let matched = if extract_symbol(state, context, symbol)?.is_some() { MatchInstance::Match { constraints: vec![], equalities: vec![] } diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs index a916fe5b..b42c87ea 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_type_error.rs @@ -14,7 +14,7 @@ fn is_stuck(state: &mut CheckState, context: &CheckContext, id: TypeId) -> where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; Ok(matches!(context.lookup_type(id), Type::Unification(_))) } @@ -26,7 +26,7 @@ fn extract_symbol_text( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; if let Type::String(_, smol_str_id) = context.lookup_type(id) { Ok(Some(context.queries.lookup_smol_str(smol_str_id))) } else { @@ -42,7 +42,7 @@ fn extract_symbol_with_kind( where Q: ExternalQueries, { - let id = normalise::normalise_expand(state, context, id)?; + let id = normalise::expand(state, context, id)?; if let Type::String(kind, smol_id) = context.lookup_type(id) { Ok(Some((kind, context.queries.lookup_smol_str(smol_id)))) } else { @@ -82,7 +82,7 @@ fn render_doc( where Q: ExternalQueries, { - let doc = normalise::normalise_expand(state, context, doc)?; + let doc = normalise::expand(state, context, doc)?; if matches!(context.lookup_type(doc), Type::Unification(_)) { return Ok(None); diff --git a/compiler-core/checking2/src/core/constraint/given.rs b/compiler-core/checking2/src/core/constraint/given.rs index 5fd64a71..93bdc36e 100644 --- a/compiler-core/checking2/src/core/constraint/given.rs +++ b/compiler-core/checking2/src/core/constraint/given.rs @@ -121,8 +121,8 @@ fn match_given_type( where Q: ExternalQueries, { - let wanted = normalise::normalise_expand(state, context, wanted)?; - let given = normalise::normalise_expand(state, context, given)?; + let wanted = normalise::expand(state, context, wanted)?; + let given = normalise::expand(state, context, given)?; if wanted == given { return Ok(MatchType::Match); @@ -189,7 +189,7 @@ where result.and_then(|| match_given_type(state, context, wanted_tail, given_tail)) } (Some(wanted_tail), None) => { - let wanted_tail = normalise::normalise_expand(state, context, wanted_tail)?; + let wanted_tail = normalise::expand(state, context, wanted_tail)?; if matches!(context.lookup_type(wanted_tail), Type::Unification(_)) { Ok(MatchType::Stuck) } else { @@ -197,7 +197,7 @@ where } } (None, Some(given_tail)) => { - let given_tail = normalise::normalise_expand(state, context, given_tail)?; + let given_tail = normalise::expand(state, context, given_tail)?; if matches!(context.lookup_type(given_tail), Type::Unification(_)) { Ok(MatchType::Stuck) } else { diff --git a/compiler-core/checking2/src/core/constraint/instances.rs b/compiler-core/checking2/src/core/constraint/instances.rs index a4b6cad8..7e0f1a55 100644 --- a/compiler-core/checking2/src/core/constraint/instances.rs +++ b/compiler-core/checking2/src/core/constraint/instances.rs @@ -281,8 +281,8 @@ fn match_type( where Q: ExternalQueries, { - let wanted = normalise::normalise_expand(state, context, wanted)?; - let given = normalise::normalise_expand(state, context, given)?; + let wanted = normalise::expand(state, context, wanted)?; + let given = normalise::expand(state, context, given)?; if wanted == given { return Ok(MatchType::Match); diff --git a/compiler-core/checking2/src/core/exhaustive.rs b/compiler-core/checking2/src/core/exhaustive.rs index 62ff3145..da1e82ec 100644 --- a/compiler-core/checking2/src/core/exhaustive.rs +++ b/compiler-core/checking2/src/core/exhaustive.rs @@ -999,7 +999,7 @@ where let mut current_id = applied_type; safe_loop! { - current_id = normalise::normalise_expand(state, context, current_id)?; + current_id = normalise::expand(state, context, current_id)?; match context.lookup_type(current_id) { Type::Application(function, argument) => { arguments.push(argument); @@ -1033,7 +1033,7 @@ where let mut arguments_iter = arguments.as_ref().iter().copied(); safe_loop! { - type_id = normalise::normalise_expand(state, context, type_id)?; + type_id = normalise::expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); diff --git a/compiler-core/checking2/src/core/exhaustive/convert.rs b/compiler-core/checking2/src/core/exhaustive/convert.rs index b5bde6ad..fa5e2c9c 100644 --- a/compiler-core/checking2/src/core/exhaustive/convert.rs +++ b/compiler-core/checking2/src/core/exhaustive/convert.rs @@ -152,7 +152,7 @@ where return Ok(None); }; - let row_type_id = normalise::normalise_expand(state, context, *row_type_id)?; + let row_type_id = normalise::expand(state, context, *row_type_id)?; let row_fields = if let Type::Row(row_type_id) = context.lookup_type(row_type_id) { context.lookup_row_type(row_type_id).fields @@ -353,7 +353,7 @@ where let mut arguments = vec![]; safe_loop! { - type_id = normalise::normalise_expand(state, context, type_id)?; + type_id = normalise::expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Application(function, argument) => { arguments.push(argument); @@ -378,5 +378,5 @@ fn normalise_expand_type( where Q: ExternalQueries, { - normalise::normalise_expand(state, context, type_id) + normalise::expand(state, context, type_id) } diff --git a/compiler-core/checking2/src/core/generalise.rs b/compiler-core/checking2/src/core/generalise.rs index f882392a..e2d47394 100644 --- a/compiler-core/checking2/src/core/generalise.rs +++ b/compiler-core/checking2/src/core/generalise.rs @@ -220,7 +220,7 @@ where (name, kind) } UnificationState::Solved(solution) => { - let solution = normalise::normalise_expand(state, context, solution)?; + let solution = normalise::expand(state, context, solution)?; let Type::Rigid(name, _, kind) = context.lookup_type(solution) else { continue; }; diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index 716b8f3d..87bbc2b2 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -119,6 +119,24 @@ where Ok(id) } +pub fn expand( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + let expanded = expand_synonym(state, context, id)?; + let normalised = normalise(state, context, expanded)?; + if normalised == id { + return Ok(id); + } + id = normalised; + } +} + /// Expands [`Type::SynonymApplication`] with respect to oversaturation. /// /// In certain cases, type synonyms can be oversaturated or applied with more @@ -251,21 +269,3 @@ where Ok(substituted) } - -pub fn normalise_expand( - state: &mut CheckState, - context: &CheckContext, - mut id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - safe_loop! { - let expanded = expand_synonym(state, context, id)?; - let normalised = normalise(state, context, expanded)?; - if normalised == id { - return Ok(id); - } - id = normalised; - } -} diff --git a/compiler-core/checking2/src/core/signature.rs b/compiler-core/checking2/src/core/signature.rs index 8cecda89..b42c9493 100644 --- a/compiler-core/checking2/src/core/signature.rs +++ b/compiler-core/checking2/src/core/signature.rs @@ -34,7 +34,7 @@ where let mut arguments = vec![]; safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; match context.lookup_type(current) { Type::Forall(binder_id, inner) => { @@ -66,14 +66,14 @@ where } let function_argument = - normalise::normalise_expand(state, context, function_argument)?; + normalise::expand(state, context, function_argument)?; let Type::Application(function, argument) = context.lookup_type(function_argument) else { return Ok(DecomposedSignature { binders, constraints, arguments, result: current }); }; - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function == context.prim.function { arguments.push(argument); current = result; diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index fda57fcb..6d867191 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -49,7 +49,7 @@ where let mut arguments = vec![]; safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Application(function, argument) => { arguments.push(argument); @@ -77,7 +77,7 @@ where let mut arguments = vec![]; safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Application(function, argument) => { arguments.push(crate::core::KindOrType::Type(argument)); @@ -278,7 +278,7 @@ where let mut current = id; safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; @@ -315,7 +315,7 @@ where let mut current = id; safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; if let InspectMode::Some(required) = mode && arguments.len() >= required @@ -329,14 +329,14 @@ where current = result; } Type::Application(function_argument, result) => { - let function_argument = normalise::normalise_expand(state, context, function_argument)?; + let function_argument = normalise::expand(state, context, function_argument)?; let Type::Application(function, argument) = context.lookup_type(function_argument) else { return Ok(InspectFunction { arguments, result: current }); }; - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function == context.prim.function { arguments.push(argument); current = result; @@ -364,7 +364,7 @@ where let mut constraints = vec![]; safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; match context.lookup_type(current) { Type::Constrained(constraint, constrained) => { constraints.push(constraint); @@ -396,7 +396,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; let Type::Forall(binder_id, inner) = context.lookup_type(id) else { break; @@ -422,7 +422,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; let Type::Forall(binder_id, inner) = context.lookup_type(id) else { break; @@ -448,7 +448,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_wanted(constraint); @@ -469,7 +469,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_given(constraint); @@ -490,7 +490,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(_, constrained) => { id = constrained; @@ -521,7 +521,7 @@ pub fn decompose_function( where Q: ExternalQueries, { - let t = normalise::normalise_expand(state, context, t)?; + let t = normalise::expand(state, context, t)?; match context.lookup_type(t) { Type::Function(argument, result) => Ok(Some((argument, result))), @@ -537,9 +537,9 @@ where } Type::Application(partial, result) => { - let partial = normalise::normalise_expand(state, context, partial)?; + let partial = normalise::expand(state, context, partial)?; if let Type::Application(constructor, argument) = context.lookup_type(partial) { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.function { return Ok(Some((argument, result))); } @@ -607,7 +607,7 @@ where Q: ExternalQueries, { safe_loop! { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Constructor(file_id, item_id) => return Ok(Some((file_id, item_id))), Type::Application(function, _) | Type::KindApplication(function, _) => { @@ -673,7 +673,7 @@ where let mut arguments = arguments.iter().copied(); safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 43386b59..7e9c0121 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -111,8 +111,8 @@ where P: SubtypePolicy, Q: ExternalQueries, { - let t1 = normalise::normalise_expand(state, context, t1)?; - let t2 = normalise::normalise_expand(state, context, t2)?; + let t1 = normalise::expand(state, context, t1)?; + let t2 = normalise::expand(state, context, t2)?; if t1 == t2 { return Ok(true); @@ -207,8 +207,8 @@ where Type::Application(t1_function, t1_argument), Type::Application(t2_function, t2_argument), ) if t1_function == context.prim.record && t2_function == context.prim.record => { - let t1_argument = normalise::normalise_expand(state, context, t1_argument)?; - let t2_argument = normalise::normalise_expand(state, context, t2_argument)?; + let t1_argument = normalise::expand(state, context, t1_argument)?; + let t2_argument = normalise::expand(state, context, t2_argument)?; let t1_argument_core = context.queries.lookup_type(t1_argument); let t2_argument_core = context.queries.lookup_type(t2_argument); @@ -235,8 +235,8 @@ pub fn unify( where Q: ExternalQueries, { - let t1 = normalise::normalise_expand(state, context, t1)?; - let t2 = normalise::normalise_expand(state, context, t2)?; + let t1 = normalise::expand(state, context, t1)?; + let t2 = normalise::expand(state, context, t2)?; if t1 == t2 { return Ok(true); @@ -386,8 +386,8 @@ pub fn can_unify( where Q: ExternalQueries, { - let t1 = normalise::normalise_expand(state, context, t1)?; - let t2 = normalise::normalise_expand(state, context, t2)?; + let t1 = normalise::expand(state, context, t1)?; + let t2 = normalise::expand(state, context, t2)?; if t1 == t2 { return Ok(CanUnify::Equal); diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs index 9297bf93..49b3e130 100644 --- a/compiler-core/checking2/src/source/binder.rs +++ b/compiler-core/checking2/src/source/binder.rs @@ -314,7 +314,7 @@ where Q: ExternalQueries, { loop { - id = normalise::normalise_expand(state, context, id)?; + id = normalise::expand(state, context, id)?; match context.lookup_type(id) { Type::Constrained(constraint, constrained) => { state.push_wanted(constraint); @@ -337,7 +337,7 @@ where Q: ExternalQueries, F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { - let function_t = normalise::normalise_expand(state, context, function_t)?; + let function_t = normalise::expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Function(argument_type, result_type) => { @@ -377,10 +377,10 @@ where } Type::Application(partial, result_type) => { - let partial = normalise::normalise_expand(state, context, partial)?; + let partial = normalise::expand(state, context, partial)?; match context.lookup_type(partial) { Type::Application(constructor, argument_type) => { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.function { check_argument(state, context, argument_id, argument_type)?; return Ok(result_type); @@ -520,15 +520,15 @@ fn extract_expected_row( where Q: ExternalQueries, { - let expected_type = normalise::normalise_expand(state, context, expected_type)?; + let expected_type = normalise::expand(state, context, expected_type)?; let Type::Application(function, argument) = context.lookup_type(expected_type) else { return Ok(None); }; - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function != context.prim.record { return Ok(None); } - let row = normalise::normalise_expand(state, context, argument)?; + let row = normalise::expand(state, context, argument)?; let Type::Row(row_id) = context.lookup_type(row) else { return Ok(None); }; @@ -548,10 +548,10 @@ where { let pattern_items = collect_pattern_items(record); - let expected_type = normalise::normalise_expand(state, context, expected_type)?; + let expected_type = normalise::expand(state, context, expected_type)?; let expected_row = if let Type::Application(function, _) = context.lookup_type(expected_type) { - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function == context.prim.record { extract_expected_row(state, context, expected_type)? } else { diff --git a/compiler-core/checking2/src/source/derive/field.rs b/compiler-core/checking2/src/source/derive/field.rs index 63efcd48..e83a05f7 100644 --- a/compiler-core/checking2/src/source/derive/field.rs +++ b/compiler-core/checking2/src/source/derive/field.rs @@ -56,7 +56,7 @@ where let mut arguments = arguments.iter().copied(); loop { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; @@ -82,11 +82,11 @@ fn generate_constraint( where Q: ExternalQueries, { - let type_id = normalise::normalise_expand(state, context, type_id)?; + let type_id = normalise::expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Application(function, argument) => { - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function == context.prim.record { generate_constraint(state, context, argument, class, class1)?; } else if is_type_to_type_variable(state, context, function)? { @@ -121,7 +121,7 @@ fn is_type_to_type_variable( where Q: ExternalQueries, { - let type_id = normalise::normalise_expand(state, context, type_id)?; + let type_id = normalise::expand(state, context, type_id)?; let kind = match context.lookup_type(type_id) { Type::Rigid(_, _, kind) => kind, Type::Unification(unification_id) => state.unifications.get(unification_id).kind, diff --git a/compiler-core/checking2/src/source/derive/generic.rs b/compiler-core/checking2/src/source/derive/generic.rs index a25c3281..460745fc 100644 --- a/compiler-core/checking2/src/source/derive/generic.rs +++ b/compiler-core/checking2/src/source/derive/generic.rs @@ -150,7 +150,7 @@ where { let mut arguments = vec![]; safe_loop! { - applied_type = normalise::normalise_expand(state, context, applied_type)?; + applied_type = normalise::expand(state, context, applied_type)?; match context.lookup_type(applied_type) { Type::Application(function, argument) | Type::KindApplication(function, argument) => { arguments.push(argument); @@ -176,7 +176,7 @@ where let mut arguments = arguments.iter().copied(); safe_loop! { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index 63613b43..aace554f 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -101,7 +101,7 @@ where let mut names = vec![]; loop { - current = normalise::normalise_expand(state, context, current)?; + current = normalise::expand(state, context, current)?; let Type::Forall(binder_id, inner) = context.lookup_type(current) else { break; }; @@ -153,7 +153,7 @@ fn check_variance_field( where Q: ExternalQueries, { - let type_id = normalise::normalise_expand(state, context, type_id)?; + let type_id = normalise::expand(state, context, type_id)?; match context.lookup_type(type_id) { Type::Rigid(name, _, _) => { @@ -166,7 +166,7 @@ where check_variance_field(state, context, result, variance, rigids)?; } Type::Application(function, argument) => { - let function = normalise::normalise_expand(state, context, function)?; + let function = normalise::expand(state, context, function)?; if function == context.prim.record { check_variance_field(state, context, argument, variance, rigids)?; } else { diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index 5c7b1c1c..b1b3f264 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -161,7 +161,7 @@ fn infer_roles<'a, 'q, Q>( where Q: ExternalQueries, { - let type_id = normalise::normalise_expand(inference.state, inference.context, type_id)?; + let type_id = normalise::expand(inference.state, inference.context, type_id)?; match inference.context.lookup_type(type_id) { Type::Rigid(name, _, kind) => { @@ -179,7 +179,7 @@ where Type::Application(function, argument) => { let function_id = - normalise::normalise_expand(inference.state, inference.context, function)?; + normalise::expand(inference.state, inference.context, function)?; let is_type_variable = matches!(inference.context.lookup_type(function_id), Type::Rigid(_, _, _)); diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs index a169bf91..afe73c36 100644 --- a/compiler-core/checking2/src/source/terms/application.rs +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -52,7 +52,7 @@ where Q: ExternalQueries, F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { - let function_t = normalise::normalise_expand(state, context, function_t)?; + let function_t = normalise::expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Function(argument_type, result_type) => { @@ -92,10 +92,10 @@ where } Type::Application(partial, result_type) => { - let partial = normalise::normalise_expand(state, context, partial)?; + let partial = normalise::expand(state, context, partial)?; match context.lookup_type(partial) { Type::Application(constructor, argument_type) => { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.function { check_argument(state, context, argument_id, argument_type)?; return Ok(result_type); @@ -173,7 +173,7 @@ pub fn check_function_type_application( where Q: ExternalQueries, { - let function_t = normalise::normalise_expand(state, context, function_t)?; + let function_t = normalise::expand(state, context, function_t)?; match context.lookup_type(function_t) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); diff --git a/compiler-core/checking2/src/source/terms/collections.rs b/compiler-core/checking2/src/source/terms/collections.rs index 70325e91..bdc10d60 100644 --- a/compiler-core/checking2/src/source/terms/collections.rs +++ b/compiler-core/checking2/src/source/terms/collections.rs @@ -35,9 +35,9 @@ pub fn check_array( where Q: ExternalQueries, { - let normalised = normalise::normalise_expand(state, context, expected)?; + let normalised = normalise::expand(state, context, expected)?; if let Type::Application(constructor, element_type) = context.lookup_type(normalised) { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.array { for expression in array.iter() { super::check_expression(state, context, *expression, element_type)?; @@ -105,11 +105,11 @@ pub fn check_record( where Q: ExternalQueries, { - let normalised = normalise::normalise_expand(state, context, expected)?; + let normalised = normalise::expand(state, context, expected)?; if let Type::Application(constructor, row_type) = context.lookup_type(normalised) { - let constructor = normalise::normalise_expand(state, context, constructor)?; + let constructor = normalise::expand(state, context, constructor)?; if constructor == context.prim.record { - let row_type = normalise::normalise_expand(state, context, row_type)?; + let row_type = normalise::expand(state, context, row_type)?; if let Type::Row(row_id) = context.lookup_type(row_type) { let expected_fields = context.lookup_row_type(row_id); From 14178b0c564af4238dc37488139972d2ea19d10e Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 17:09:18 +0800 Subject: [PATCH 372/386] Fix lookup function delegation --- compiler-core/checking2/src/core/normalise.rs | 8 ++++---- compiler-core/checking2/src/core/unification.rs | 14 +++++++------- compiler-core/checking2/src/core/walk.rs | 8 ++++---- compiler-core/checking2/src/safety.rs | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index 87bbc2b2..78fe929e 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -29,7 +29,7 @@ where } fn reduce_once(&mut self, id: TypeId) -> Option { - let t = self.context.queries.lookup_type(id); + let t = self.context.lookup_type(id); if let Some(next) = self.rule_prune_unifications(&t) { return Some(next); @@ -61,14 +61,14 @@ where return None; }; - let row = self.context.queries.lookup_row_type(row_id); + let row = self.context.lookup_row_type(row_id); if row.fields.is_empty() { return row.tail; } let tail_id = row.tail?; - let tail_t = self.context.queries.lookup_type(tail_id); + let tail_t = self.context.lookup_type(tail_id); let Type::Row(inner_row_id) = tail_t else { return None; @@ -78,7 +78,7 @@ where return None; } - let inner = self.context.queries.lookup_row_type(inner_row_id); + let inner = self.context.lookup_row_type(inner_row_id); let merged_fields = { let left = row.fields.iter().cloned(); diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 7e9c0121..20d3c160 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -118,8 +118,8 @@ where return Ok(true); } - let t1_core = context.queries.lookup_type(t1); - let t2_core = context.queries.lookup_type(t2); + let t1_core = context.lookup_type(t1); + let t2_core = context.lookup_type(t2); match (t1_core, t2_core) { // Function subtyping is contravariant on the argument type and @@ -210,8 +210,8 @@ where let t1_argument = normalise::expand(state, context, t1_argument)?; let t2_argument = normalise::expand(state, context, t2_argument)?; - let t1_argument_core = context.queries.lookup_type(t1_argument); - let t2_argument_core = context.queries.lookup_type(t2_argument); + let t1_argument_core = context.lookup_type(t1_argument); + let t2_argument_core = context.lookup_type(t2_argument); if let (Type::Row(t1_row_id), Type::Row(t2_row_id)) = (t1_argument_core, t2_argument_core) @@ -242,8 +242,8 @@ where return Ok(true); } - let t1_core = context.queries.lookup_type(t1); - let t2_core = context.queries.lookup_type(t2); + let t1_core = context.lookup_type(t1); + let t2_core = context.lookup_type(t2); let unifies = match (t1_core, t2_core) { // PureScript has an impredicative type system i.e. unification @@ -568,7 +568,7 @@ where Q: ExternalQueries, { let id = normalise::normalise(state, context, id)?; - let t = context.queries.lookup_type(id); + let t = context.lookup_type(id); match t { Type::Application(function, argument) | Type::KindApplication(function, argument) => { diff --git a/compiler-core/checking2/src/core/walk.rs b/compiler-core/checking2/src/core/walk.rs index 6c9cb20c..4e366c53 100644 --- a/compiler-core/checking2/src/core/walk.rs +++ b/compiler-core/checking2/src/core/walk.rs @@ -36,7 +36,7 @@ where W: TypeWalker, { let id = normalise(state, context, id)?; - let t = context.queries.lookup_type(id); + let t = context.lookup_type(id); if let WalkAction::Stop = walker.visit(state, context, id, &t)? { return Ok(()); @@ -48,7 +48,7 @@ where walk_type(state, context, argument, walker)?; } Type::SynonymApplication(synonym_id) => { - let synonym = context.queries.lookup_synonym(synonym_id); + let synonym = context.lookup_synonym(synonym_id); for application in synonym.arguments.iter() { let argument = match application { KindOrType::Kind(argument) | KindOrType::Type(argument) => *argument, @@ -57,7 +57,7 @@ where } } Type::Forall(binder_id, inner) => { - let binder = context.queries.lookup_forall_binder(binder_id); + let binder = context.lookup_forall_binder(binder_id); walker.visit_binder(&binder); walk_type(state, context, binder.kind, walker)?; walk_type(state, context, inner, walker)?; @@ -77,7 +77,7 @@ where Type::Constructor(_, _) => {} Type::Integer(_) | Type::String(_, _) => {} Type::Row(row_id) => { - let row = context.queries.lookup_row_type(row_id); + let row = context.lookup_row_type(row_id); for field in row.fields.iter() { walk_type(state, context, field.id, walker)?; } diff --git a/compiler-core/checking2/src/safety.rs b/compiler-core/checking2/src/safety.rs index 33d8b8d5..faea58eb 100644 --- a/compiler-core/checking2/src/safety.rs +++ b/compiler-core/checking2/src/safety.rs @@ -14,7 +14,7 @@ pub const FUEL: u32 = 1_000_000; /// let mut current_id = type_id; /// safe_loop! { /// current_id = normalise(current_id); -/// if let Type::Application(function, _) = context.queries.lookup_type(current_id) { +/// if let Type::Application(function, _) = context.lookup_type(current_id) { /// current_id = function; /// } else { /// break; From 5dc01b032db45b89fb1896b28f4affe2c1a08919 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 19:28:45 +0800 Subject: [PATCH 373/386] Add documentation to normalise and expand --- compiler-core/checking2/src/core/fold.rs | 7 +++---- compiler-core/checking2/src/core/normalise.rs | 15 +++++++++++++++ compiler-core/checking2/src/core/walk.rs | 5 ++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/compiler-core/checking2/src/core/fold.rs b/compiler-core/checking2/src/core/fold.rs index 5b9c91aa..4d440993 100644 --- a/compiler-core/checking2/src/core/fold.rs +++ b/compiler-core/checking2/src/core/fold.rs @@ -5,8 +5,7 @@ use std::sync::Arc; use building_types::QueryResult; use crate::context::CheckContext; -use crate::core::normalise::normalise; -use crate::core::{ForallBinder, KindOrType, Type, TypeId}; +use crate::core::{ForallBinder, KindOrType, Type, TypeId, normalise}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -38,14 +37,14 @@ where Q: ExternalQueries, F: TypeFold, { - let mut id = normalise(state, context, id)?; + let mut id = normalise::normalise(state, context, id)?; safe_loop! { let t = context.lookup_type(id); match folder.transform(state, context, id, &t)? { FoldAction::Replace(id) => return Ok(id), FoldAction::ReplaceThen(then_id) => { - let then_id = normalise(state, context, then_id)?; + let then_id = normalise::normalise(state, context, then_id)?; if then_id == id { break; } diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index 78fe929e..7c2326be 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -94,6 +94,16 @@ where } } +/// Normalises a [`Type`] head. +/// +/// Notably, this function applies the following rules: +/// 1. Replaces solved unfiication variables, compressing them +/// if they solve to other solved unification variables. +/// 2. Simplifies row types, such as merging concrete row tails, +/// or extracting an empty row's tail as a standalone type. +/// +/// This function should be used in checking rules where +/// synonyms must remain opaque such as in kind checking. pub fn normalise( state: &mut CheckState, context: &CheckContext, @@ -119,6 +129,11 @@ where Ok(id) } +/// Expands [`Type::SynonymApplication`] heads. +/// +/// This function also applies normalisation using [`normalise`], +/// and should be used in checking rules where synonyms must be +/// transparent and inspected. pub fn expand( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/core/walk.rs b/compiler-core/checking2/src/core/walk.rs index 4e366c53..4d9c99f4 100644 --- a/compiler-core/checking2/src/core/walk.rs +++ b/compiler-core/checking2/src/core/walk.rs @@ -4,8 +4,7 @@ use building_types::QueryResult; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::normalise::normalise; -use crate::core::{ForallBinder, KindOrType, Type, TypeId}; +use crate::core::{ForallBinder, KindOrType, Type, TypeId, normalise}; use crate::state::CheckState; pub enum WalkAction { @@ -35,7 +34,7 @@ where Q: ExternalQueries, W: TypeWalker, { - let id = normalise(state, context, id)?; + let id = normalise::normalise(state, context, id)?; let t = context.lookup_type(id); if let WalkAction::Stop = walker.visit(state, context, id, &t)? { From 98b56b14136dfc8d59a7691a7d5126701639a6ac Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 7 Mar 2026 19:38:49 +0800 Subject: [PATCH 374/386] Use normalise::expand in more checking rules --- compiler-core/checking2/src/core/unification.rs | 4 ++-- compiler-core/checking2/src/source/derive/variance.rs | 2 +- compiler-core/checking2/src/source/terms/application.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler-core/checking2/src/core/unification.rs b/compiler-core/checking2/src/core/unification.rs index 20d3c160..458de1cb 100644 --- a/compiler-core/checking2/src/core/unification.rs +++ b/compiler-core/checking2/src/core/unification.rs @@ -488,7 +488,7 @@ pub fn solve( where Q: ExternalQueries, { - let solution = normalise::normalise(state, context, solution)?; + let solution = normalise::expand(state, context, solution)?; match promote_type(state, context, id, solution)? { PromoteResult::Ok => {} @@ -567,7 +567,7 @@ where where Q: ExternalQueries, { - let id = normalise::normalise(state, context, id)?; + let id = normalise::expand(state, context, id)?; let t = context.lookup_type(id); match t { diff --git a/compiler-core/checking2/src/source/derive/variance.rs b/compiler-core/checking2/src/source/derive/variance.rs index aace554f..56d45b77 100644 --- a/compiler-core/checking2/src/source/derive/variance.rs +++ b/compiler-core/checking2/src/source/derive/variance.rs @@ -236,7 +236,7 @@ fn contains_rigid_name( where Q: ExternalQueries, { - let type_id = normalise::normalise(state, context, type_id)?; + let type_id = normalise::expand(state, context, type_id)?; Ok(match context.lookup_type(type_id) { Type::Application(function, argument) | Type::KindApplication(function, argument) => { contains_rigid_name(state, context, function, name)? diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs index afe73c36..d51b4d5a 100644 --- a/compiler-core/checking2/src/source/terms/application.rs +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -73,7 +73,7 @@ where Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; + let binder_kind = normalise::expand(state, context, binder.kind)?; let replacement = state.fresh_unification(context.queries, binder_kind); let function_t = SubstituteName::one(state, context, binder.name, replacement, inner)?; @@ -177,7 +177,7 @@ where match context.lookup_type(function_t) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; + let binder_kind = normalise::expand(state, context, binder.kind)?; let (argument_type, _) = types::check_kind(state, context, argument, binder_kind)?; SubstituteName::one(state, context, binder.name, argument_type, inner) From 3e18281361998cb1255463323e462987d78e86d8 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Mar 2026 00:13:15 +0800 Subject: [PATCH 375/386] Use analysis patterns for function application --- compiler-core/checking2/src/source/binder.rs | 118 +-------- compiler-core/checking2/src/source/roles.rs | 3 +- .../checking2/src/source/terms/application.rs | 249 +++++++++++------- .../checking2/src/source/terms/form_ado.rs | 68 ++--- .../checking2/src/source/terms/form_do.rs | 64 ++--- 5 files changed, 216 insertions(+), 286 deletions(-) diff --git a/compiler-core/checking2/src/source/binder.rs b/compiler-core/checking2/src/source/binder.rs index 49b3e130..251aeb4b 100644 --- a/compiler-core/checking2/src/source/binder.rs +++ b/compiler-core/checking2/src/source/binder.rs @@ -8,9 +8,9 @@ use smol_str::SmolStr; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::substitute::SubstituteName; use crate::core::{RowField, RowType, Type, TypeId, normalise, toolkit, unification}; use crate::error::{ErrorCrumb, ErrorKind}; +use crate::source::terms::application; use crate::source::{operator, types}; use crate::state::CheckState; @@ -167,7 +167,7 @@ where let inferred_type = if arguments.is_empty() { constructor_t = toolkit::instantiate_unifications(state, context, constructor_t)?; - collect_wanteds(state, context, constructor_t)? + toolkit::without_constraints(state, context, constructor_t)? } else { for &argument in arguments.iter() { constructor_t = check_constructor_binder_application( @@ -304,118 +304,24 @@ where } } -/// Collects wanteds from a constrained type, returning the inner type. -fn collect_wanteds( - state: &mut CheckState, - context: &CheckContext, - mut id: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - loop { - id = normalise::expand(state, context, id)?; - match context.lookup_type(id) { - Type::Constrained(constraint, constrained) => { - state.push_wanted(constraint); - id = constrained; - } - _ => return Ok(id), - } - } -} - -/// Applies a constructor type to a binder argument. -pub fn check_function_application_core( - state: &mut CheckState, - context: &CheckContext, - function_t: TypeId, - argument_id: A, - check_argument: F, -) -> QueryResult -where - Q: ExternalQueries, - F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, -{ - let function_t = normalise::expand(state, context, function_t)?; - - match context.lookup_type(function_t) { - Type::Function(argument_type, result_type) => { - check_argument(state, context, argument_id, argument_type)?; - Ok(result_type) - } - - Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); - let function_u = context.intern_function(argument_u, result_u); - - unification::solve(state, context, function_t, unification_id, function_u)?; - check_argument(state, context, argument_id, argument_u)?; - - Ok(result_u) - } - - Type::Forall(binder_id, inner) => { - let binder = context.lookup_forall_binder(binder_id); - let binder_kind = normalise::normalise(state, context, binder.kind)?; - - let replacement = state.fresh_unification(context.queries, binder_kind); - let function_t = SubstituteName::one(state, context, binder.name, replacement, inner)?; - check_function_application_core(state, context, function_t, argument_id, check_argument) - } - - Type::Constrained(constraint, constrained) => { - state.push_wanted(constraint); - check_function_application_core( - state, - context, - constrained, - argument_id, - check_argument, - ) - } - - Type::Application(partial, result_type) => { - let partial = normalise::expand(state, context, partial)?; - match context.lookup_type(partial) { - Type::Application(constructor, argument_type) => { - let constructor = normalise::expand(state, context, constructor)?; - if constructor == context.prim.function { - check_argument(state, context, argument_id, argument_type)?; - return Ok(result_type); - } - if let Type::Unification(unification_id) = context.lookup_type(constructor) { - unification::solve( - state, - context, - constructor, - unification_id, - context.prim.function, - )?; - check_argument(state, context, argument_id, argument_type)?; - return Ok(result_type); - } - Ok(context.unknown("invalid function application")) - } - _ => Ok(context.unknown("invalid function application")), - } - } - - _ => Ok(context.unknown("invalid function application")), - } -} - fn check_constructor_binder_application( state: &mut CheckState, context: &CheckContext, - constructor_t: TypeId, + constructor: TypeId, binder_id: lowering::BinderId, ) -> QueryResult where Q: ExternalQueries, { - check_function_application_core(state, context, constructor_t, binder_id, check_binder) + let Some(application::ApplicationAnalysis { argument, result, .. }) = + application::analyse_function_application(state, context, constructor)? + else { + return Ok(context.unknown("invalid function application")); + }; + + check_binder(state, context, binder_id, argument)?; + + Ok(result) } enum PatternItem { diff --git a/compiler-core/checking2/src/source/roles.rs b/compiler-core/checking2/src/source/roles.rs index b1b3f264..fdea9303 100644 --- a/compiler-core/checking2/src/source/roles.rs +++ b/compiler-core/checking2/src/source/roles.rs @@ -178,8 +178,7 @@ where } Type::Application(function, argument) => { - let function_id = - normalise::expand(inference.state, inference.context, function)?; + let function_id = normalise::expand(inference.state, inference.context, function)?; let is_type_variable = matches!(inference.context.lookup_type(function_id), Type::Rigid(_, _, _)); diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs index d51b4d5a..7a0cbf61 100644 --- a/compiler-core/checking2/src/source/terms/application.rs +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -1,74 +1,53 @@ +use std::mem; +use std::ops::ControlFlow; + use building_types::QueryResult; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{Type, TypeId, normalise, unification}; use crate::source::types; use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; -pub fn infer_infix_chain( - state: &mut CheckState, - context: &CheckContext, - head: lowering::ExpressionId, - tail: &[lowering::InfixPair], -) -> QueryResult -where - Q: ExternalQueries, -{ - let mut infix_type = super::infer_expression(state, context, head)?; - - for lowering::InfixPair { tick, element } in tail.iter() { - let Some(tick) = tick else { return Ok(context.unknown("missing infix tick")) }; - let Some(element) = element else { return Ok(context.unknown("missing infix element")) }; - - let tick_type = super::infer_expression(state, context, *tick)?; - let applied_tick = check_function_application_core( - state, - context, - tick_type, - infix_type, - |state, context, infix_type, expected_type| { - unification::subtype(state, context, infix_type, expected_type)?; - Ok(infix_type) - }, - )?; - - infix_type = check_function_term_application(state, context, applied_tick, *element)?; - } +pub struct ApplicationAnalysis { + pub constraints: Vec, + pub argument: TypeId, + pub result: TypeId, +} - Ok(infix_type) +pub struct GenericApplication { + pub argument: TypeId, + pub result: TypeId, } -/// Generic function for application checking. -pub fn check_function_application_core( +fn analyse_function_application_step( state: &mut CheckState, context: &CheckContext, - function_t: TypeId, - argument_id: A, - check_argument: F, -) -> QueryResult + function: TypeId, + constraints: &mut Vec, +) -> QueryResult, TypeId>> where Q: ExternalQueries, - F: FnOnce(&mut CheckState, &CheckContext, A, TypeId) -> QueryResult, { - let function_t = normalise::expand(state, context, function_t)?; - - match context.lookup_type(function_t) { - Type::Function(argument_type, result_type) => { - check_argument(state, context, argument_id, argument_type)?; - Ok(result_type) + match context.lookup_type(function) { + Type::Function(argument, result) => { + let analysis = + ApplicationAnalysis { constraints: mem::take(constraints), argument, result }; + Ok(ControlFlow::Break(Some(analysis))) } Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); - let function_u = context.intern_function(argument_u, result_u); + let argument = state.fresh_unification(context.queries, context.prim.t); + let result = state.fresh_unification(context.queries, context.prim.t); + let function = context.intern_function(argument, result); + + unification::solve(state, context, function, unification_id, function)?; - unification::solve(state, context, function_t, unification_id, function_u)?; - check_argument(state, context, argument_id, argument_u)?; + let analysis = + ApplicationAnalysis { constraints: mem::take(constraints), argument, result }; - Ok(result_u) + Ok(ControlFlow::Break(Some(analysis))) } Type::Forall(binder_id, inner) => { @@ -76,67 +55,89 @@ where let binder_kind = normalise::expand(state, context, binder.kind)?; let replacement = state.fresh_unification(context.queries, binder_kind); - let function_t = SubstituteName::one(state, context, binder.name, replacement, inner)?; - check_function_application_core(state, context, function_t, argument_id, check_argument) + let function = SubstituteName::one(state, context, binder.name, replacement, inner)?; + Ok(ControlFlow::Continue(function)) } Type::Constrained(constraint, constrained) => { - state.push_wanted(constraint); - check_function_application_core( - state, - context, - constrained, - argument_id, - check_argument, - ) + constraints.push(constraint); + Ok(ControlFlow::Continue(constrained)) } - Type::Application(partial, result_type) => { - let partial = normalise::expand(state, context, partial)?; - match context.lookup_type(partial) { - Type::Application(constructor, argument_type) => { - let constructor = normalise::expand(state, context, constructor)?; - if constructor == context.prim.function { - check_argument(state, context, argument_id, argument_type)?; - return Ok(result_type); - } - if let Type::Unification(unification_id) = context.lookup_type(constructor) { - unification::solve( - state, - context, - constructor, - unification_id, - context.prim.function, - )?; - check_argument(state, context, argument_id, argument_type)?; - return Ok(result_type); - } - Ok(context.unknown("invalid function application")) - } - _ => Ok(context.unknown("invalid function application")), + Type::Application(function_argument, result) => { + let function_argument = normalise::expand(state, context, function_argument)?; + + let Type::Application(constructor, argument) = context.lookup_type(function_argument) + else { + return Ok(ControlFlow::Break(None)); + }; + + let constructor = normalise::expand(state, context, constructor)?; + if constructor == context.prim.function { + let analysis = + ApplicationAnalysis { constraints: mem::take(constraints), argument, result }; + return Ok(ControlFlow::Break(Some(analysis))); + } + + if let Type::Unification(unification_id) = context.lookup_type(constructor) { + unification::solve( + state, + context, + constructor, + unification_id, + context.prim.function, + )?; + + let analysis = + ApplicationAnalysis { constraints: mem::take(constraints), argument, result }; + + return Ok(ControlFlow::Break(Some(analysis))); } + + Ok(ControlFlow::Break(None)) } - _ => Ok(context.unknown("invalid function application")), + _ => Ok(ControlFlow::Break(None)), } } -pub fn check_function_term_application( +pub fn analyse_function_application( state: &mut CheckState, context: &CheckContext, - function_t: TypeId, - expression_id: lowering::ExpressionId, -) -> QueryResult + mut function: TypeId, +) -> QueryResult> +where + Q: ExternalQueries, +{ + let mut constraints = vec![]; + safe_loop! { + function = normalise::expand(state, context, function)?; + match analyse_function_application_step(state, context, function, &mut constraints)? { + ControlFlow::Continue(next) => function = next, + ControlFlow::Break(analysis) => return Ok(analysis), + }; + } +} + +pub fn check_generic_application( + state: &mut CheckState, + context: &CheckContext, + function: TypeId, +) -> QueryResult> where Q: ExternalQueries, { - check_function_application_core( - state, - context, - function_t, - expression_id, - super::check_expression, - ) + let Some(ApplicationAnalysis { constraints, argument, result }) = + analyse_function_application(state, context, function)? + else { + return Ok(None); + }; + + for constraint in constraints { + state.push_wanted(constraint); + } + + Ok(Some(GenericApplication { argument, result })) } pub fn check_function_application( @@ -164,24 +165,72 @@ where } } +pub fn check_function_term_application( + state: &mut CheckState, + context: &CheckContext, + function: TypeId, + expression_id: lowering::ExpressionId, +) -> QueryResult +where + Q: ExternalQueries, +{ + let Some(GenericApplication { argument, result }) = + check_generic_application(state, context, function)? + else { + return Ok(context.unknown("invalid function application")); + }; + super::check_expression(state, context, expression_id, argument)?; + Ok(result) +} + pub fn check_function_type_application( state: &mut CheckState, context: &CheckContext, - function_t: TypeId, + function: TypeId, argument: lowering::TypeId, ) -> QueryResult where Q: ExternalQueries, { - let function_t = normalise::expand(state, context, function_t)?; - match context.lookup_type(function_t) { + let function = normalise::expand(state, context, function)?; + match context.lookup_type(function) { Type::Forall(binder_id, inner) => { let binder = context.lookup_forall_binder(binder_id); let binder_kind = normalise::expand(state, context, binder.kind)?; - let (argument_type, _) = types::check_kind(state, context, argument, binder_kind)?; - SubstituteName::one(state, context, binder.name, argument_type, inner) + let (argument, _) = types::check_kind(state, context, argument, binder_kind)?; + SubstituteName::one(state, context, binder.name, argument, inner) } _ => Ok(context.unknown("invalid type application")), } } + +pub fn infer_infix_chain( + state: &mut CheckState, + context: &CheckContext, + head: lowering::ExpressionId, + tail: &[lowering::InfixPair], +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut infix_type = super::infer_expression(state, context, head)?; + + for lowering::InfixPair { tick, element } in tail.iter() { + let Some(tick) = tick else { return Ok(context.unknown("missing infix tick")) }; + let Some(element) = element else { return Ok(context.unknown("missing infix element")) }; + + let tick_type = super::infer_expression(state, context, *tick)?; + let Some(GenericApplication { argument, result }) = + check_generic_application(state, context, tick_type)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, infix_type, argument)?; + let applied_tick = result; + + infix_type = check_function_term_application(state, context, applied_tick, *element)?; + } + + Ok(infix_type) +} diff --git a/compiler-core/checking2/src/source/terms/form_ado.rs b/compiler-core/checking2/src/source/terms/form_ado.rs index 01289ab4..95c473f2 100644 --- a/compiler-core/checking2/src/source/terms/form_ado.rs +++ b/compiler-core/checking2/src/source/terms/form_ado.rs @@ -216,27 +216,21 @@ where { let expression_type = super::infer_expression(state, context, expression)?; - let map_applied = application::check_function_application_core( - state, - context, - map_type, - lambda_type, - |state, context, lambda_type, expected_type| { - unification::subtype(state, context, lambda_type, expected_type)?; - Ok(lambda_type) - }, - )?; + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, map_type)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, lambda_type, argument)?; + + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, result)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, expression_type, argument)?; - application::check_function_application_core( - state, - context, - map_applied, - expression_type, - |state, context, expression_type, expected_type| { - unification::subtype(state, context, expression_type, expected_type)?; - Ok(expression_type) - }, - ) + Ok(result) } pub fn infer_ado_apply_core( @@ -251,25 +245,19 @@ where { let expression_type = super::infer_expression(state, context, expression)?; - let apply_applied = application::check_function_application_core( - state, - context, - apply_type, - continuation_type, - |state, context, continuation_type, expected_type| { - unification::subtype(state, context, continuation_type, expected_type)?; - Ok(continuation_type) - }, - )?; + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, apply_type)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, continuation_type, argument)?; + + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, result)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, expression_type, argument)?; - application::check_function_application_core( - state, - context, - apply_applied, - expression_type, - |state, context, expression_type, expected_type| { - unification::subtype(state, context, expression_type, expected_type)?; - Ok(expression_type) - }, - ) + Ok(result) } diff --git a/compiler-core/checking2/src/source/terms/form_do.rs b/compiler-core/checking2/src/source/terms/form_do.rs index 352e6b52..d12b68ef 100644 --- a/compiler-core/checking2/src/source/terms/form_do.rs +++ b/compiler-core/checking2/src/source/terms/form_do.rs @@ -379,27 +379,21 @@ where let expression_type = super::infer_expression(state, context, expression)?; let lambda_type = context.intern_function(binder_type, continuation_type); - let bind_applied = application::check_function_application_core( - state, - context, - bind_type, - expression_type, - |state, context, expression_type, expected_type| { - unification::subtype(state, context, expression_type, expected_type)?; - Ok(expression_type) - }, - )?; + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, bind_type)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, expression_type, argument)?; - application::check_function_application_core( - state, - context, - bind_applied, - lambda_type, - |state, context, lambda_type, expected_type| { - unification::subtype(state, context, lambda_type, expected_type)?; - Ok(lambda_type) - }, - ) + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, result)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, lambda_type, argument)?; + + Ok(result) } pub fn infer_do_discard_core( @@ -414,26 +408,20 @@ where { let expression_type = super::infer_expression(state, context, expression)?; - let discard_applied = application::check_function_application_core( - state, - context, - discard_type, - expression_type, - |state, context, expression_type, expected_type| { - unification::subtype(state, context, expression_type, expected_type)?; - Ok(expression_type) - }, - )?; + let Some(application::GenericApplication { argument, result }) = + application::check_generic_application(state, context, discard_type)? + else { + return Ok(context.unknown("invalid function application")); + }; + unification::subtype(state, context, expression_type, argument)?; - let result_type = application::check_function_application_core( - state, - context, - discard_applied, - (), - |_, _, _, continuation_type| Ok(continuation_type), - )?; + let Some(application::GenericApplication { result, .. }) = + application::check_generic_application(state, context, result)? + else { + return Ok(context.unknown("invalid function application")); + }; - unification::subtype(state, context, continuation_type, result_type)?; + unification::subtype(state, context, continuation_type, result)?; Ok(continuation_type) } From d1a1b8fec309c59efb7b8d14ac0521952c962b8c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sun, 8 Mar 2026 21:41:27 +0800 Subject: [PATCH 376/386] Fix synonym applications in operator checking --- .../checking2/src/source/operator.rs | 16 +- compiler-core/checking2/src/source/synonym.rs | 243 +++++++++++++----- .../016_type_operator_chain_infer/Main.snap | 8 +- .../Main.snap | 8 +- .../226_equation_synonym_expansion/Main.purs | 11 + .../226_equation_synonym_expansion/Main.snap | 15 ++ .../tests/checking2/generated.rs | 2 + 7 files changed, 232 insertions(+), 71 deletions(-) create mode 100644 tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.purs create mode 100644 tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.snap diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 95a20d4b..433f134b 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -11,7 +11,7 @@ use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{Type, TypeId, normalise, toolkit, unification}; use crate::error::ErrorKind; -use crate::source::{binder, terms, types}; +use crate::source::{binder, synonym, terms, types}; use crate::state::CheckState; use crate::{ExternalQueries, OperatorBranchTypes}; @@ -356,6 +356,20 @@ impl IsOperator for lowering::TypeId { }; let operator_kind = toolkit::lookup_file_type(state, context, file_id, item_id)?; + + if let Some((elaborated_type, result_kind)) = + synonym::try_check_resolved_synonym_application( + state, + context, + (target_file_id, target_item_id), + operator_kind, + &[(left, left_kind), (right, right_kind)], + )? + { + let result_kind = normalise::normalise(state, context, result_kind)?; + return Ok((elaborated_type, result_kind)); + } + let function_type = context.queries.intern_type(Type::Constructor(target_file_id, target_item_id)); let (function_type, function_kind) = diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index d4dd8e83..e1c0fa75 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -1,19 +1,23 @@ //! Implements syntax-driven checking rules for synonym detection. use std::mem; +use std::ops::ControlFlow; use std::sync::Arc; use building_types::QueryResult; use files::FileId; use indexing::TypeItemId; +use itertools::Itertools; -use crate::ExternalQueries; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; -use crate::core::{KindOrType, Saturation, Synonym, Type, TypeId, normalise, toolkit, unification}; +use crate::core::{ + CheckedSynonym, KindOrType, Saturation, Synonym, Type, TypeId, normalise, toolkit, unification, +}; use crate::error::ErrorKind; use crate::source::types; use crate::state::CheckState; +use crate::{ExternalQueries, safe_loop}; #[derive(Debug, Clone, Copy)] pub struct ParsedSynonym { @@ -23,6 +27,12 @@ pub struct ParsedSynonym { pub arity: usize, } +#[derive(Debug, Clone, Copy)] +enum Argument { + Syntax(lowering::TypeId), + Core(TypeId, TypeId), +} + pub fn parse_synonym( state: &mut CheckState, context: &CheckContext, @@ -41,14 +51,13 @@ where return Ok(None); }; - let Some(checked_synonym) = toolkit::lookup_file_synonym(state, context, file_id, type_id)? + let Some(CheckedSynonym { kind, parameters, .. }) = + toolkit::lookup_file_synonym(state, context, file_id, type_id)? else { return Ok(None); }; - let kind = checked_synonym.kind; - let arity = checked_synonym.parameters.len(); - + let arity = parameters.len(); Ok(Some(ParsedSynonym { file_id, type_id, kind, arity })) } @@ -100,11 +109,7 @@ pub fn infer_synonym_application( where Q: ExternalQueries, { - let ParsedSynonym { file_id, type_id, kind: function_kind, arity } = synonym; - - if is_recursive_synonym(context, file_id, type_id)? { - state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, type_id }); - } + let ParsedSynonym { file_id, type_id, kind, arity } = synonym; if arguments.len() < arity { if state.defer_expansion { @@ -112,7 +117,7 @@ where state, context, (file_id, type_id), - function_kind, + kind, arguments, ); } @@ -122,19 +127,75 @@ where return Ok((unknown, unknown)); } - let (synonym_arguments, excess_arguments) = arguments.split_at(arity); - let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let arguments = arguments.iter().copied().map(Argument::Syntax).collect_vec(); let defer_expansion = mem::replace(&mut state.defer_expansion, true); - let chain_result = infer_synonym_application_chain( - state, - context, - (function_type, function_kind), - synonym_arguments, - ); + let checked = check_synonym_application_arguments(state, context, synonym, &arguments); state.defer_expansion = defer_expansion; - let (applications, (_, synonym_kind)) = chain_result?; + checked +} + +pub fn check_synonym_application( + state: &mut CheckState, + context: &CheckContext, + synonym: ParsedSynonym, + arguments: &[(TypeId, TypeId)], +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let arguments = arguments + .iter() + .copied() + .map(|(type_id, kind_id)| Argument::Core(type_id, kind_id)) + .collect_vec(); + + check_synonym_application_arguments(state, context, synonym, &arguments) +} + +pub fn try_check_resolved_synonym_application( + state: &mut CheckState, + context: &CheckContext, + (file_id, type_id): (FileId, TypeItemId), + kind: TypeId, + arguments: &[(TypeId, TypeId)], +) -> QueryResult> +where + Q: ExternalQueries, +{ + let Some(CheckedSynonym { parameters, .. }) = + toolkit::lookup_file_synonym(state, context, file_id, type_id)? + else { + return Ok(None); + }; + + let synonym = ParsedSynonym { file_id, type_id, kind, arity: parameters.len() }; + let checked = check_synonym_application(state, context, synonym, arguments)?; + Ok(Some(checked)) +} + +fn check_synonym_application_arguments( + state: &mut CheckState, + context: &CheckContext, + synonym: ParsedSynonym, + arguments: &[Argument], +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + let ParsedSynonym { file_id, type_id, kind, arity } = synonym; + debug_assert!(arguments.len() >= arity); + + if is_recursive_synonym(context, file_id, type_id)? { + state.insert_error(ErrorKind::RecursiveSynonymExpansion { file_id, type_id }); + } + + let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let (synonym_arguments, excess_arguments) = arguments.split_at(arity); + + let (applications, (_, synonym_kind)) = + collect_synonym_applications(state, context, (function_type, kind), synonym_arguments)?; let synonym = Synonym { saturation: Saturation::Full, @@ -147,8 +208,14 @@ where let mut synonym_kind = synonym_kind; for &argument in excess_arguments { - (synonym_type, synonym_kind) = - types::infer_application_kind(state, context, (synonym_type, synonym_kind), argument)?; + let mut applications = vec![]; + (synonym_type, synonym_kind) = apply_synonym_argument( + state, + context, + &mut applications, + (synonym_type, synonym_kind), + argument, + )?; } Ok((synonym_type, synonym_kind)) @@ -158,15 +225,17 @@ fn infer_partial_synonym_application( state: &mut CheckState, context: &CheckContext, (file_id, type_id): (FileId, TypeItemId), - function_kind: TypeId, + kind: TypeId, arguments: &[lowering::TypeId], ) -> QueryResult<(TypeId, TypeId)> where Q: ExternalQueries, { let function_type = context.queries.intern_type(Type::Constructor(file_id, type_id)); + let arguments = arguments.iter().copied().map(Argument::Syntax).collect_vec(); + let (applications, (_, synonym_kind)) = - infer_synonym_application_chain(state, context, (function_type, function_kind), arguments)?; + collect_synonym_applications(state, context, (function_type, kind), &arguments)?; let synonym = Synonym { saturation: Saturation::Partial, @@ -180,65 +249,78 @@ where Ok((synonym_type, synonym_kind)) } -fn infer_synonym_application_chain( +fn collect_synonym_applications( state: &mut CheckState, context: &CheckContext, - function: (TypeId, TypeId), - arguments: &[lowering::TypeId], + mut function: (TypeId, TypeId), + arguments: &[Argument], ) -> QueryResult<(Vec, (TypeId, TypeId))> where Q: ExternalQueries, { let mut applications = vec![]; - let mut current_function = function; - for &argument_id in arguments { - current_function = infer_synonym_application_kind( - state, - context, - &mut applications, - current_function, - argument_id, - )?; + for &argument in arguments { + function = apply_synonym_argument(state, context, &mut applications, function, argument)?; } - Ok((applications, current_function)) + Ok((applications, function)) } -fn infer_synonym_application_kind( +fn apply_synonym_argument_step( state: &mut CheckState, context: &CheckContext, applications: &mut Vec, (function_type, function_kind): (TypeId, TypeId), - argument: lowering::TypeId, -) -> QueryResult<(TypeId, TypeId)> + argument: Argument, +) -> QueryResult> where Q: ExternalQueries, { - let function_kind = normalise::normalise(state, context, function_kind)?; - match context.lookup_type(function_kind) { - Type::Function(argument_kind, result_kind) => { - let (argument_type, _) = types::check_kind(state, context, argument, argument_kind)?; - let result_kind = normalise::normalise(state, context, result_kind)?; + Type::Function(function_argument, function_result) => { + let argument_type = match argument { + Argument::Syntax(argument_id) => { + let (argument_type, _) = + types::check_kind(state, context, argument_id, function_argument)?; + argument_type + } + Argument::Core(argument_type, argument_kind) => { + unification::subtype(state, context, argument_kind, function_argument)?; + argument_type + } + }; + let result_type = context.intern_application(function_type, argument_type); + let result_kind = normalise::normalise(state, context, function_result)?; + applications.push(KindOrType::Type(argument_type)); - Ok((result_type, result_kind)) + Ok(ControlFlow::Break((result_type, result_kind))) } Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); + let function_argument = state.fresh_unification(context.queries, context.prim.t); + let function_result = state.fresh_unification(context.queries, context.prim.t); + let function = context.intern_function(function_argument, function_result); + unification::solve(state, context, function_kind, unification_id, function)?; + + let argument_type = match argument { + Argument::Syntax(argument_id) => { + let (argument_type, _) = + types::check_kind(state, context, argument_id, function_argument)?; + argument_type + } + Argument::Core(argument_type, checked_kind) => { + unification::subtype(state, context, checked_kind, function_argument)?; + argument_type + } + }; - let function_u = context.intern_function(argument_u, result_u); - unification::solve(state, context, function_kind, unification_id, function_u)?; - - let (argument_type, _) = types::check_kind(state, context, argument, argument_u)?; - let result_kind = normalise::normalise(state, context, result_u)?; let result_type = context.intern_application(function_type, argument_type); + let result_kind = normalise::normalise(state, context, function_result)?; applications.push(KindOrType::Type(argument_type)); - Ok((result_type, result_kind)) + Ok(ControlFlow::Break((result_type, result_kind))) } Type::Forall(binder_id, inner_kind) => { @@ -246,25 +328,26 @@ where let binder_kind = normalise::normalise(state, context, binder.kind)?; let kind_argument = state.fresh_unification(context.queries, binder_kind); + let function_type = context.intern_kind_application(function_type, kind_argument); let function_kind = SubstituteName::one(state, context, binder.name, kind_argument, inner_kind)?; applications.push(KindOrType::Kind(kind_argument)); - infer_synonym_application_kind( - state, - context, - applications, - (function_type, function_kind), - argument, - ) + Ok(ControlFlow::Continue((function_type, function_kind))) } _ => { - let (argument_type, _) = types::infer_kind(state, context, argument)?; + let argument_type = match argument { + Argument::Syntax(argument_id) => { + let (argument_type, _) = types::infer_kind(state, context, argument_id)?; + argument_type + } + Argument::Core(argument_type, _) => argument_type, + }; - let t = context.intern_application(function_type, argument_type); - let k = context.unknown("cannot apply synonym type"); + let invalid_type = context.intern_application(function_type, argument_type); + let unknown_kind = context.unknown("cannot apply synonym type"); { let function_type = state.pretty_id(context, function_type)?; @@ -279,11 +362,39 @@ where } applications.push(KindOrType::Type(argument_type)); - Ok((t, k)) + Ok(ControlFlow::Break((invalid_type, unknown_kind))) } } } +fn apply_synonym_argument( + state: &mut CheckState, + context: &CheckContext, + applications: &mut Vec, + (mut function_type, mut function_kind): (TypeId, TypeId), + argument: Argument, +) -> QueryResult<(TypeId, TypeId)> +where + Q: ExternalQueries, +{ + safe_loop! { + function_kind = normalise::normalise(state, context, function_kind)?; + match apply_synonym_argument_step( + state, + context, + applications, + (function_type, function_kind), + argument, + )? { + ControlFlow::Break(result) => break Ok(result), + ControlFlow::Continue((next_type, next_kind)) => { + function_type = next_type; + function_kind = next_kind; + } + }; + } +} + fn is_recursive_synonym( context: &CheckContext, file_id: FileId, diff --git a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap index 068ea780..3462e6a7 100644 --- a/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap +++ b/tests-integration/fixtures/checking2/016_type_operator_chain_infer/Main.snap @@ -17,9 +17,13 @@ ChainParen :: Synonyms type Add a b = (a :: (t3 :: Type)) -type Chain a b c = Add @(t9 :: Type) @(t7 :: Type) +type Chain a b c = Add + @(t9 :: Type) + @(t7 :: Type) (Add @(t9 :: Type) @(t8 :: Type) (a :: (t9 :: Type)) (b :: (t8 :: Type))) (c :: (t7 :: Type)) -type ChainParen a b c = Add @(t15 :: Type) @(t13 :: Type) +type ChainParen a b c = Add + @(t15 :: Type) + @(t13 :: Type) (Add @(t15 :: Type) @(t14 :: Type) (a :: (t15 :: Type)) (b :: (t14 :: Type))) (c :: (t13 :: Type)) diff --git a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap index 81f00a18..58c900c1 100644 --- a/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap +++ b/tests-integration/fixtures/checking2/019_type_operator_chain_precedence/Main.snap @@ -20,9 +20,13 @@ ChainParen :: Synonyms type Add a b = (a :: (t3 :: Type)) type Mul a b = (a :: (t7 :: Type)) -type Chain a b c = Add @(t13 :: Type) @(t12 :: Type) +type Chain a b c = Add + @(t13 :: Type) + @(t12 :: Type) (a :: (t13 :: Type)) (Mul @(t12 :: Type) @(t11 :: Type) (b :: (t12 :: Type)) (c :: (t11 :: Type))) -type ChainParen a b c = Mul @(t19 :: Type) @(t17 :: Type) +type ChainParen a b c = Mul + @(t19 :: Type) + @(t17 :: Type) (Add @(t19 :: Type) @(t18 :: Type) (a :: (t19 :: Type)) (b :: (t18 :: Type))) (c :: (t17 :: Type)) diff --git a/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.purs b/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.purs new file mode 100644 index 00000000..4082ba07 --- /dev/null +++ b/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.purs @@ -0,0 +1,11 @@ +module Main where + +type NaturalTransformation :: (Type -> Type) -> (Type -> Type) -> Type +type NaturalTransformation f g = forall a. f a -> g a + +infixr 5 type NaturalTransformation as ~> + +foreign import unsafeCoerce :: forall a b. a -> b + +test :: forall f. Array ~> f +test a = unsafeCoerce a diff --git a/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.snap b/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.snap new file mode 100644 index 00000000..d6b06870 --- /dev/null +++ b/tests-integration/fixtures/checking2/226_equation_synonym_expansion/Main.snap @@ -0,0 +1,15 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +unsafeCoerce :: forall (a :: Type) (b :: Type). (a :: Type) -> (b :: Type) +test :: forall (f :: Type -> Type). NaturalTransformation Array (f :: Type -> Type) + +Types +NaturalTransformation :: (Type -> Type) -> (Type -> Type) -> Type +~> :: (Type -> Type) -> (Type -> Type) -> Type + +Synonyms +type NaturalTransformation f g = forall (a :: Type). (f :: Type -> Type) (a :: Type) -> (g :: Type -> Type) (a :: Type) diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 049419e3..27159f52 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -475,3 +475,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_224_derive_newtype_insufficient_params_main() { run_test("224_derive_newtype_insufficient_params", "Main"); } #[rustfmt::skip] #[test] fn test_225_derive_newtype_class_insufficient_params_main() { run_test("225_derive_newtype_class_insufficient_params", "Main"); } + +#[rustfmt::skip] #[test] fn test_226_equation_synonym_expansion_main() { run_test("226_equation_synonym_expansion", "Main"); } From 9034def1beabb5225255d84a81cf138bd508a04f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Mon, 9 Mar 2026 03:18:59 +0800 Subject: [PATCH 377/386] Extract utility for application checking --- compiler-core/checking2/src/core/toolkit.rs | 24 +++++++++++ .../checking2/src/source/operator.rs | 35 ++++++++-------- compiler-core/checking2/src/source/synonym.rs | 39 +++++++++--------- .../checking2/src/source/terms/application.rs | 6 +-- compiler-core/checking2/src/source/types.rs | 40 +++++++++---------- 5 files changed, 80 insertions(+), 64 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 6d867191..3bc39096 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -13,6 +13,7 @@ use crate::core::{ CheckedClass, CheckedInstance, CheckedSynonym, ForallBinder, KindOrType, Role, Type, TypeId, constraint, normalise, unification, }; +use crate::error::ErrorKind; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -38,6 +39,29 @@ pub struct DecomposedInstance { pub arguments: Vec, } +pub fn report_invalid_type_application( + state: &mut CheckState, + context: &CheckContext, + function_type: TypeId, + function_kind: TypeId, + argument_type: TypeId, +) -> QueryResult<()> +where + Q: ExternalQueries, +{ + let function_type = state.pretty_id(context, function_type)?; + let function_kind = state.pretty_id(context, function_kind)?; + let argument_type = state.pretty_id(context, argument_type)?; + + state.insert_error(ErrorKind::InvalidTypeApplication { + function_type, + function_kind, + argument_type, + }); + + Ok(()) +} + pub fn extract_type_application( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index 433f134b..e602b8b7 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -10,10 +10,9 @@ use sugar::bracketing::BracketingResult; use crate::context::CheckContext; use crate::core::substitute::SubstituteName; use crate::core::{Type, TypeId, normalise, toolkit, unification}; -use crate::error::ErrorKind; use crate::source::{binder, synonym, terms, types}; use crate::state::CheckState; -use crate::{ExternalQueries, OperatorBranchTypes}; +use crate::{ExternalQueries, OperatorBranchTypes, safe_loop}; #[derive(Copy, Clone, Debug)] enum OperatorKindMode { @@ -479,9 +478,8 @@ fn apply_type_argument( where Q: ExternalQueries, { - loop { + safe_loop! { function_kind = normalise::normalise(state, context, function_kind)?; - match context.lookup_type(function_kind) { Type::Function(expected_kind, result_kind) => { unification::subtype(state, context, argument_kind, expected_kind)?; @@ -491,10 +489,10 @@ where } Type::Unification(unification_id) => { - let result_u = state.fresh_unification(context.queries, context.prim.t); - let function_u = context.intern_function(argument_kind, result_u); - unification::solve(state, context, function_kind, unification_id, function_u)?; - function_kind = result_u; + let result_kind = state.fresh_unification(context.queries, context.prim.t); + let function_type = context.intern_function(argument_kind, result_kind); + unification::solve(state, context, function_kind, unification_id, function_type)?; + function_kind = result_kind; } Type::Forall(binder_id, inner_kind) => { @@ -507,17 +505,16 @@ where } _ => { - let invalid = context.intern_application(function_type, argument_type); - let function_type_message = state.pretty_id(context, function_type)?; - let function_kind_message = state.pretty_id(context, function_kind)?; - let argument_type_message = state.pretty_id(context, argument_type)?; - state.insert_error(ErrorKind::InvalidTypeApplication { - function_type: function_type_message, - function_kind: function_kind_message, - argument_type: argument_type_message, - }); - let unknown = context.unknown("cannot apply operator type"); - return Ok((invalid, unknown)); + let invalid_type = context.intern_application(function_type, argument_type); + toolkit::report_invalid_type_application( + state, + context, + function_type, + function_kind, + argument_type, + )?; + let unknown_kind = context.unknown("cannot apply operator type"); + return Ok((invalid_type, unknown_kind)); } } } diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index e1c0fa75..06d5c60f 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -278,46 +278,47 @@ where Q: ExternalQueries, { match context.lookup_type(function_kind) { - Type::Function(function_argument, function_result) => { + Type::Function(expected_kind, result_kind) => { let argument_type = match argument { Argument::Syntax(argument_id) => { let (argument_type, _) = - types::check_kind(state, context, argument_id, function_argument)?; + types::check_kind(state, context, argument_id, expected_kind)?; argument_type } Argument::Core(argument_type, argument_kind) => { - unification::subtype(state, context, argument_kind, function_argument)?; + unification::subtype(state, context, argument_kind, expected_kind)?; argument_type } }; let result_type = context.intern_application(function_type, argument_type); - let result_kind = normalise::normalise(state, context, function_result)?; + let result_kind = normalise::normalise(state, context, result_kind)?; applications.push(KindOrType::Type(argument_type)); Ok(ControlFlow::Break((result_type, result_kind))) } Type::Unification(unification_id) => { - let function_argument = state.fresh_unification(context.queries, context.prim.t); - let function_result = state.fresh_unification(context.queries, context.prim.t); - let function = context.intern_function(function_argument, function_result); + let argument_kind = state.fresh_unification(context.queries, context.prim.t); + let result_kind = state.fresh_unification(context.queries, context.prim.t); + + let function = context.intern_function(argument_kind, result_kind); unification::solve(state, context, function_kind, unification_id, function)?; let argument_type = match argument { Argument::Syntax(argument_id) => { let (argument_type, _) = - types::check_kind(state, context, argument_id, function_argument)?; + types::check_kind(state, context, argument_id, argument_kind)?; argument_type } Argument::Core(argument_type, checked_kind) => { - unification::subtype(state, context, checked_kind, function_argument)?; + unification::subtype(state, context, checked_kind, argument_kind)?; argument_type } }; let result_type = context.intern_application(function_type, argument_type); - let result_kind = normalise::normalise(state, context, function_result)?; + let result_kind = normalise::normalise(state, context, result_kind)?; applications.push(KindOrType::Type(argument_type)); Ok(ControlFlow::Break((result_type, result_kind))) @@ -349,17 +350,13 @@ where let invalid_type = context.intern_application(function_type, argument_type); let unknown_kind = context.unknown("cannot apply synonym type"); - { - let function_type = state.pretty_id(context, function_type)?; - let function_kind = state.pretty_id(context, function_kind)?; - let argument_type = state.pretty_id(context, argument_type)?; - - state.insert_error(ErrorKind::InvalidTypeApplication { - function_type, - function_kind, - argument_type, - }); - } + toolkit::report_invalid_type_application( + state, + context, + function_type, + function_kind, + argument_type, + )?; applications.push(KindOrType::Type(argument_type)); Ok(ControlFlow::Break((invalid_type, unknown_kind))) diff --git a/compiler-core/checking2/src/source/terms/application.rs b/compiler-core/checking2/src/source/terms/application.rs index 7a0cbf61..bdb87eb5 100644 --- a/compiler-core/checking2/src/source/terms/application.rs +++ b/compiler-core/checking2/src/source/terms/application.rs @@ -143,7 +143,7 @@ where pub fn check_function_application( state: &mut CheckState, context: &CheckContext, - function_t: TypeId, + function_type: TypeId, argument: &lowering::ExpressionArgument, ) -> QueryResult where @@ -154,13 +154,13 @@ where let Some(type_argument) = type_argument else { return Ok(context.unknown("missing type argument")); }; - check_function_type_application(state, context, function_t, *type_argument) + check_function_type_application(state, context, function_type, *type_argument) } lowering::ExpressionArgument::Term(term_argument) => { let Some(term_argument) = term_argument else { return Ok(context.unknown("missing term argument")); }; - check_function_term_application(state, context, function_t, *term_argument) + check_function_term_application(state, context, function_type, *term_argument) } } } diff --git a/compiler-core/checking2/src/source/types.rs b/compiler-core/checking2/src/source/types.rs index 5183c189..3dc50508 100644 --- a/compiler-core/checking2/src/source/types.rs +++ b/compiler-core/checking2/src/source/types.rs @@ -10,7 +10,7 @@ use crate::core::substitute::SubstituteName; use crate::core::{ ForallBinder, KindOrType, RowField, RowType, Type, TypeId, normalise, toolkit, unification, }; -use crate::error::{ErrorCrumb, ErrorKind}; +use crate::error::ErrorCrumb; use crate::source::{operator, synonym}; use crate::state::CheckState; use crate::{ExternalQueries, safe_loop}; @@ -394,25 +394,25 @@ where Type::Function(argument_kind, result_kind) => { let (argument_type, _) = check_kind(state, context, argument, argument_kind)?; - let t = context.intern_application(function_type, argument_type); - let k = normalise::normalise(state, context, result_kind)?; + let result_type = context.intern_application(function_type, argument_type); + let result_kind = normalise::normalise(state, context, result_kind)?; - Ok((t, k)) + Ok((result_type, result_kind)) } Type::Unification(unification_id) => { - let argument_u = state.fresh_unification(context.queries, context.prim.t); - let result_u = state.fresh_unification(context.queries, context.prim.t); + let argument_kind = state.fresh_unification(context.queries, context.prim.t); + let result_kind = state.fresh_unification(context.queries, context.prim.t); - let function_u = context.intern_function(argument_u, result_u); - unification::solve(state, context, function_kind, unification_id, function_u)?; + let function = context.intern_function(argument_kind, result_kind); + unification::solve(state, context, function_kind, unification_id, function)?; - let (argument_type, _) = check_kind(state, context, argument, argument_u)?; + let (argument_type, _) = check_kind(state, context, argument, argument_kind)?; - let t = context.intern_application(function_type, argument_type); - let k = normalise::normalise(state, context, result_u)?; + let result_type = context.intern_application(function_type, argument_type); + let result_kind = normalise::normalise(state, context, result_kind)?; - Ok((t, k)) + Ok((result_type, result_kind)) } Type::Forall(binder_id, inner_kind) => { @@ -433,20 +433,18 @@ where // this ensures that implicit variables are bound. let (argument_type, _) = infer_kind(state, context, argument)?; - let t = context.intern_application(function_type, argument_type); - let k = context.unknown("cannot apply function type"); + let result_type = context.intern_application(function_type, argument_type); + let result_kind = context.unknown("cannot apply function type"); - let function_type = state.pretty_id(context, function_type)?; - let function_kind = state.pretty_id(context, function_kind)?; - let argument_type = state.pretty_id(context, argument_type)?; - - state.insert_error(ErrorKind::InvalidTypeApplication { + toolkit::report_invalid_type_application( + state, + context, function_type, function_kind, argument_type, - }); + )?; - Ok((t, k)) + Ok((result_type, result_kind)) } } } From 798c9d2e9f480ed772d3008c3ef6859451840c2c Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 10 Mar 2026 21:47:00 +0800 Subject: [PATCH 378/386] Implement try_peel_trailing_rigids --- .../core/constraint/compiler/prim_coerce.rs | 5 +- compiler-core/checking2/src/core/toolkit.rs | 21 +++++-- .../checking2/src/source/derive/newtype.rs | 63 ++++++++++++++++--- .../Main.snap | 12 ---- .../123_derive_newtype_function/Main.snap | 22 ------- .../Main.purs | 13 ++++ .../Main.snap | 39 ++++++++++++ .../tests/checking2/generated.rs | 2 + 8 files changed, 128 insertions(+), 49 deletions(-) create mode 100644 tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.purs create mode 100644 tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.snap diff --git a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs index b4976957..fb0ee8be 100644 --- a/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs +++ b/compiler-core/checking2/src/core/constraint/compiler/prim_coerce.rs @@ -126,7 +126,8 @@ where && toolkit::is_newtype(context, file_id, item_id)? { if toolkit::is_constructor_in_scope(context, file_id, item_id)? { - if let Some(inner) = toolkit::get_newtype_inner(state, context, file_id, item_id, left)? + if let Some(toolkit::NewtypeInner { inner, .. }) = + toolkit::get_newtype_inner(state, context, file_id, item_id, left)? { let constraint = make_coercible_constraint(context, inner, right); return Ok(NewtypeCoercionResult::Success(MatchInstance::Match { @@ -144,7 +145,7 @@ where && toolkit::is_newtype(context, file_id, item_id)? { if toolkit::is_constructor_in_scope(context, file_id, item_id)? { - if let Some(inner) = + if let Some(toolkit::NewtypeInner { inner, .. }) = toolkit::get_newtype_inner(state, context, file_id, item_id, right)? { let constraint = make_coercible_constraint(context, left, inner); diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 3bc39096..430d2273 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -39,6 +39,11 @@ pub struct DecomposedInstance { pub arguments: Vec, } +pub struct NewtypeInner { + pub inner: TypeId, + pub rigids: Vec, +} + pub fn report_invalid_type_application( state: &mut CheckState, context: &CheckContext, @@ -670,7 +675,7 @@ pub fn get_newtype_inner( newtype_file: FileId, newtype_id: TypeItemId, newtype_type: TypeId, -) -> QueryResult> +) -> QueryResult> where Q: ExternalQueries, { @@ -695,6 +700,7 @@ where let mut current = constructor_type; let mut arguments = arguments.iter().copied(); + let mut rigids = vec![]; safe_loop! { current = normalise::expand(state, context, current)?; @@ -706,10 +712,13 @@ where let replacement = arguments .next() .map(|argument| match argument { - crate::core::KindOrType::Kind(argument) - | crate::core::KindOrType::Type(argument) => argument, + KindOrType::Kind(argument) | KindOrType::Type(argument) => argument, }) - .unwrap_or_else(|| state.fresh_rigid(context.queries, binder.kind)); + .unwrap_or_else(|| { + let rigid = state.fresh_rigid(context.queries, binder.kind); + rigids.push(rigid); + rigid + }); current = SubstituteName::one(state, context, binder.name, replacement, inner)?; } @@ -717,7 +726,7 @@ where current = normalise::normalise(state, context, current)?; let InspectFunction { arguments, .. } = inspect_function(state, context, current)?; - let [argument] = arguments[..] else { return Ok(None) }; + let [inner] = arguments[..] else { return Ok(None) }; - Ok(Some(argument)) + Ok(Some(NewtypeInner { inner, rigids })) } diff --git a/compiler-core/checking2/src/source/derive/newtype.rs b/compiler-core/checking2/src/source/derive/newtype.rs index 6603d159..0af24263 100644 --- a/compiler-core/checking2/src/source/derive/newtype.rs +++ b/compiler-core/checking2/src/source/derive/newtype.rs @@ -4,7 +4,7 @@ use indexing::TypeItemId; use crate::ExternalQueries; use crate::context::CheckContext; -use crate::core::{Type, TypeId, toolkit, unification}; +use crate::core::{Type, TypeId, normalise, toolkit, unification}; use crate::error::ErrorKind; use crate::state::CheckState; @@ -38,7 +38,7 @@ where return Ok(None); } - let Some(inner_type) = + let Some(toolkit::NewtypeInner { inner, rigids }) = toolkit::get_newtype_inner(state, context, newtype_file, newtype_id, *newtype_type)? else { let type_message = state.pretty_id(context, *newtype_type)?; @@ -46,13 +46,22 @@ where return Ok(None); }; - let class_type = context.queries.intern_type(Type::Constructor(class_file, class_id)); + let inner = if let Some(inner) = try_peel_trailing_rigids(state, context, inner, &rigids)? { + inner + } else { + state.insert_error(ErrorKind::InvalidNewtypeDeriveSkolemArguments); + return Ok(None); + }; + + let inner = normalise::normalise(state, context, inner)?; + let class = context.queries.intern_type(Type::Constructor(class_file, class_id)); + let mut delegate_constraint = preceding_arguments .iter() .copied() - .fold(class_type, |function, argument| context.intern_application(function, argument)); - delegate_constraint = context.intern_application(delegate_constraint, inner_type); + .fold(class, |function, argument| context.intern_application(function, argument)); + delegate_constraint = context.intern_application(delegate_constraint, inner); Ok(Some(DeriveStrategy::NewtypeDeriveConstraint { delegate_constraint })) } @@ -84,7 +93,7 @@ where return Ok(None); } - let Some(extracted_inner) = + let Some(newtype_inner) = toolkit::get_newtype_inner(state, context, newtype_file, newtype_id, *newtype_type)? else { let type_message = state.pretty_id(context, *newtype_type)?; @@ -92,7 +101,47 @@ where return Ok(None); }; - unification::unify(state, context, *inner_type, extracted_inner)?; + unification::unify(state, context, *inner_type, newtype_inner.inner)?; Ok(Some(DeriveStrategy::HeadOnly)) } + +fn try_peel_trailing_rigids( + state: &mut CheckState, + context: &CheckContext, + mut type_id: TypeId, + rigids: &[TypeId], +) -> QueryResult> +where + Q: ExternalQueries, +{ + if rigids.is_empty() { + return Ok(Some(type_id)); + } + + for &rigid in rigids.iter().rev() { + type_id = normalise::expand(state, context, type_id)?; + + match context.lookup_type(type_id) { + Type::Application(function, argument) | Type::KindApplication(function, argument) => { + let argument = normalise::expand(state, context, argument)?; + if argument != rigid { + return Ok(None); + } + type_id = function; + } + + Type::Function(argument, result) => { + let result = normalise::expand(state, context, result)?; + if result != rigid { + return Ok(None); + } + type_id = context.intern_application(context.prim.function, argument); + } + + _ => return Ok(None), + } + } + + Ok(Some(type_id)) +} diff --git a/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap index 4ad40741..84ea88ca 100644 --- a/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap +++ b/tests-integration/fixtures/checking2/122_derive_newtype_higher_kinded/Main.snap @@ -20,15 +20,3 @@ instance Empty Array Roles Wrapper = [Representational] - -Errors -CheckError { - kind: NoInstanceFound { - constraint: Id(6), - }, - crumbs: [ - TermDeclaration( - Idx::(3), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap index 06b78edd..00641ef8 100644 --- a/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap +++ b/tests-integration/fixtures/checking2/123_derive_newtype_function/Main.snap @@ -12,25 +12,3 @@ Builder :: Type -> Type -> Type Roles Builder = [Representational, Representational] - -Errors -CheckError { - kind: NoInstanceFound { - constraint: Id(10), - }, - crumbs: [ - TermDeclaration( - Idx::(1), - ), - ], -} -CheckError { - kind: NoInstanceFound { - constraint: Id(11), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.purs b/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.purs new file mode 100644 index 00000000..6222d1a4 --- /dev/null +++ b/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.purs @@ -0,0 +1,13 @@ +module Main where + +class Empty f where + empty :: f Int + +instance Empty Array where + empty = [] + +newtype Vector n a = Vector (Array a) +derive newtype instance Empty (Vector n) + +newtype InvalidVector a n = InvalidVector (Array a) +derive newtype instance Empty (InvalidVector Int) diff --git a/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.snap b/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.snap new file mode 100644 index 00000000..d2813993 --- /dev/null +++ b/tests-integration/fixtures/checking2/227_derive_newtype_invalid_skolem_layout/Main.snap @@ -0,0 +1,39 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int +Vector :: + forall (t3 :: Type) (n :: (t3 :: Type)) (a :: Type). + Array (a :: Type) -> Vector @(t3 :: Type) (n :: (t3 :: Type)) (a :: Type) +InvalidVector :: + forall (t6 :: Type) (a :: Type) (n :: (t6 :: Type)). + Array (a :: Type) -> InvalidVector @(t6 :: Type) (a :: Type) (n :: (t6 :: Type)) + +Types +Empty :: (Type -> Type) -> Constraint +Vector :: forall (t3 :: Type). (t3 :: Type) -> Type -> Type +InvalidVector :: forall (t6 :: Type). Type -> (t6 :: Type) -> Type + +Classes +class forall (f :: Type -> Type). Empty (f :: Type -> Type) + empty :: forall (f :: Type -> Type). Empty (f :: Type -> Type) => (f :: Type -> Type) Int + +Instances +instance Empty Array + +Roles +Vector = [Phantom, Representational] +InvalidVector = [Representational, Phantom] + +Errors +CheckError { + kind: InvalidNewtypeDeriveSkolemArguments, + crumbs: [ + TermDeclaration( + Idx::(5), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 27159f52..ea0842b3 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -477,3 +477,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_225_derive_newtype_class_insufficient_params_main() { run_test("225_derive_newtype_class_insufficient_params", "Main"); } #[rustfmt::skip] #[test] fn test_226_equation_synonym_expansion_main() { run_test("226_equation_synonym_expansion", "Main"); } + +#[rustfmt::skip] #[test] fn test_227_derive_newtype_invalid_skolem_layout_main() { run_test("227_derive_newtype_invalid_skolem_layout", "Main"); } From 20d1bd3fbbffe81b263269879c5f5b9682d797b7 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Tue, 10 Mar 2026 23:59:35 +0800 Subject: [PATCH 379/386] Consider more forms in is_binary_operator_type --- compiler-core/checking2/src/core/toolkit.rs | 22 ++++++++++++++++ compiler-core/checking2/src/source.rs | 25 ------------------- .../checking2/src/source/term_items.rs | 2 +- .../checking2/src/source/type_items.rs | 2 +- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/compiler-core/checking2/src/core/toolkit.rs b/compiler-core/checking2/src/core/toolkit.rs index 430d2273..497d867e 100644 --- a/compiler-core/checking2/src/core/toolkit.rs +++ b/compiler-core/checking2/src/core/toolkit.rs @@ -378,6 +378,28 @@ where } } +pub fn is_binary_operator_type( + state: &mut CheckState, + context: &CheckContext, + mut id: TypeId, +) -> QueryResult +where + Q: ExternalQueries, +{ + safe_loop! { + id = normalise::expand(state, context, id)?; + match context.lookup_type(id) { + Type::Forall(_, inner) | Type::Constrained(_, inner) => { + id = inner; + } + _ => break, + } + } + + let inspected = inspect_function_with(state, context, id, InspectMode::Some(2))?; + Ok(inspected.arguments.len() >= 2) +} + pub fn decompose_instance( state: &mut CheckState, context: &CheckContext, diff --git a/compiler-core/checking2/src/source.rs b/compiler-core/checking2/src/source.rs index 43766c69..6f1fcfb6 100644 --- a/compiler-core/checking2/src/source.rs +++ b/compiler-core/checking2/src/source.rs @@ -13,28 +13,3 @@ mod type_items; pub use term_items::check_term_items; pub use type_items::check_type_items; - -use building_types::QueryResult; - -use crate::ExternalQueries; -use crate::context::CheckContext; -use crate::core::{TypeId, signature}; -use crate::state::CheckState; - -fn is_binary_operator_type( - state: &mut CheckState, - context: &CheckContext, - kind: TypeId, -) -> QueryResult -where - Q: ExternalQueries, -{ - let signature::DecomposedSignature { arguments, .. } = signature::decompose_signature( - state, - context, - kind, - signature::DecomposeSignatureMode::Full, - )?; - - Ok(arguments.len() == 2) -} diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index 7086edac..c7598571 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -717,7 +717,7 @@ where continue; }; - if !super::is_binary_operator_type(state, context, t)? { + if !toolkit::is_binary_operator_type(state, context, t)? { let kind_message = state.pretty_id(context, t)?; state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); } diff --git a/compiler-core/checking2/src/source/type_items.rs b/compiler-core/checking2/src/source/type_items.rs index 8a264a9b..23bf44cb 100644 --- a/compiler-core/checking2/src/source/type_items.rs +++ b/compiler-core/checking2/src/source/type_items.rs @@ -929,7 +929,7 @@ where continue; }; - if !super::is_binary_operator_type(state, context, kind)? { + if !toolkit::is_binary_operator_type(state, context, kind)? { let kind_message = state.pretty_id(context, kind)?; state.insert_error(ErrorKind::InvalidTypeOperator { kind_message }); } From f9f5cfc69fd52d28e3ea17e6bf4b566aced5c4c1 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 11 Mar 2026 02:41:02 +0800 Subject: [PATCH 380/386] Add failing test case for missing freshening The test case demonstrates unification errors that come from failed promote_type checks when trying to solve unification variables against rigid type variables that came from the instance head canonical. --- .../Main.purs | 10 ++ .../Main.snap | 120 ++++++++++++++++++ .../tests/checking2/generated.rs | 2 + 3 files changed, 132 insertions(+) create mode 100644 tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.purs create mode 100644 tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap diff --git a/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.purs b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.purs new file mode 100644 index 00000000..1c0155ac --- /dev/null +++ b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.purs @@ -0,0 +1,10 @@ +module Main where + +data Proxy :: forall k. k -> Type +data Proxy a = Proxy + +class Top a where + top :: a + +instance topProxy :: Top (Proxy a) where + top = Proxy :: Proxy a diff --git a/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap new file mode 100644 index 00000000..c6a20a62 --- /dev/null +++ b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap @@ -0,0 +1,120 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +Proxy :: forall (k :: Type) (a :: (k :: Type)). Proxy @(k :: Type) (a :: (k :: Type)) +top :: forall (a :: Type). Top (a :: Type) => (a :: Type) + +Types +Proxy :: forall (k :: Type). (k :: Type) -> Type +Top :: Type -> Constraint + +Classes +class forall (a :: Type). Top (a :: Type) + top :: forall (a :: Type). Top (a :: Type) => (a :: Type) + +Instances +instance forall (t4 :: Type) (t3 :: (t4 :: Type)). Top (Proxy @(t4 :: Type) (t3 :: (t4 :: Type))) + +Roles +Proxy = [Phantom] + +Errors +CheckError { + kind: CannotUnify { + t1: Id(6), + t2: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + InferringKind( + AstId(34), + ), + CheckingKind( + AstId(36), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + CheckingExpression( + AstId(33), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + CheckingExpression( + AstId(33), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(6), + t2: Id(7), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(12), + t2: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(11), + t2: Id(14), + }, + crumbs: [ + TermDeclaration( + Idx::(2), + ), + CheckingExpression( + AstId(32), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index ea0842b3..57db3efb 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -479,3 +479,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_226_equation_synonym_expansion_main() { run_test("226_equation_synonym_expansion", "Main"); } #[rustfmt::skip] #[test] fn test_227_derive_newtype_invalid_skolem_layout_main() { run_test("227_derive_newtype_invalid_skolem_layout", "Main"); } + +#[rustfmt::skip] #[test] fn test_228_instance_implicit_variable_freshening_main() { run_test("228_instance_implicit_variable_freshening", "Main"); } From f0894ef8cfc2bebc66c7ff73b3a56dfde217d02d Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 11 Mar 2026 02:41:02 +0800 Subject: [PATCH 381/386] Apply freshening when checking instance members This fixes the 228 test case by introducing a proper instantiation step for rigid type variables from the instance head canonical. This creates fresh rigid type variables with update depths, fixing issues with the escape check in promote_type and subsequently, unification. This also changes the representation of Bindings::implicit to be based on Vec instead of HashMap. This enables trivial shadowing, ensuring that implicit type variables are kept up to date after instantiation. --- .../checking2/src/source/term_items.rs | 201 +++++++++++------- compiler-core/checking2/src/state.rs | 68 +++++- .../Main.snap | 98 --------- 3 files changed, 182 insertions(+), 185 deletions(-) diff --git a/compiler-core/checking2/src/source/term_items.rs b/compiler-core/checking2/src/source/term_items.rs index c7598571..414ca952 100644 --- a/compiler-core/checking2/src/source/term_items.rs +++ b/compiler-core/checking2/src/source/term_items.rs @@ -217,8 +217,7 @@ where item_id, member, (class_file, class_id), - &instance.constraints, - &instance.arguments, + &instance, )?; } } @@ -233,8 +232,7 @@ fn check_instance_member_group( instance_item_id: TermItemId, member: &lowering::InstanceMemberGroup, (class_file, class_id): (FileId, TypeItemId), - instance_constraints: &[TypeId], - instance_arguments: &[TypeId], + instance: &toolkit::DecomposedInstance, ) -> QueryResult<()> where Q: ExternalQueries, @@ -246,8 +244,7 @@ where context, member, (class_file, class_id), - instance_constraints, - instance_arguments, + instance, ) }) }) @@ -258,93 +255,137 @@ fn check_instance_member_group_core( context: &CheckContext, member: &lowering::InstanceMemberGroup, (class_file, class_id): (FileId, TypeItemId), - instance_constraints: &[TypeId], - instance_arguments: &[TypeId], + instance: &toolkit::DecomposedInstance, ) -> QueryResult<()> where Q: ExternalQueries, { - for &constraint in instance_constraints { - state.push_given(constraint); - } + let FreshenedInstanceRigids { + constraints: instance_constraints, + arguments: instance_arguments, + substitution, + } = freshen_instance_rigids(state, context, instance)?; + + state.with_implicit(context, &substitution, |state| { + for &constraint in &instance_constraints { + state.push_given(constraint); + } - let class_member_type = if let Some((member_file, member_id)) = member.resolution { - Some(toolkit::lookup_file_term(state, context, member_file, member_id)?) - } else { - None - }; + let class_member_type = if let Some((member_file, member_id)) = member.resolution { + Some(toolkit::lookup_file_term(state, context, member_file, member_id)?) + } else { + None + }; - let class_member_type = if let Some(class_member_type) = class_member_type { - specialise_class_member_type( - state, - context, - class_member_type, - (class_file, class_id), - instance_arguments, - )? - } else { - None - }; + let class_member_type = if let Some(class_member_type) = class_member_type { + specialise_class_member_type( + state, + context, + class_member_type, + (class_file, class_id), + &instance_arguments, + )? + } else { + None + }; - let residuals = if let Some(signature_id) = member.signature { - let (signature_member_type, _) = - types::check_kind(state, context, signature_id, context.prim.t)?; - - if let Some(class_member_type) = class_member_type { - let unified = - unification::unify(state, context, signature_member_type, class_member_type)?; - if !unified { - let expected = state.pretty_id(context, class_member_type)?; - let actual = state.pretty_id(context, signature_member_type)?; - state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); + let residuals = if let Some(signature_id) = member.signature { + let (signature_member_type, _) = + types::check_kind(state, context, signature_id, context.prim.t)?; + + if let Some(class_member_type) = class_member_type { + let unified = + unification::unify(state, context, signature_member_type, class_member_type)?; + if !unified { + let expected = state.pretty_id(context, class_member_type)?; + let actual = state.pretty_id(context, signature_member_type)?; + state.insert_error(ErrorKind::InstanceMemberTypeMismatch { expected, actual }); + } } + + let equation_set = equations::analyse_equation_set( + state, + context, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Explicit(signature_id), + expected_type: signature_member_type, + }, + &member.equations, + )?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &member.equations, + )?; + state.report_exhaustiveness(context, exhaustiveness); + state.solve_constraints(context)? + } else if let Some(specialised_type) = class_member_type { + let equation_set = equations::analyse_equation_set( + state, + context, + equations::EquationMode::Check { + origin: equations::EquationTypeOrigin::Implicit, + expected_type: specialised_type, + }, + &member.equations, + )?; + let exhaustiveness = equations::compute_equation_exhaustiveness( + state, + context, + &equation_set, + &member.equations, + )?; + state.report_exhaustiveness(context, exhaustiveness); + state.solve_constraints(context)? + } else { + vec![] + }; + + for residual in residuals { + let constraint = state.pretty_id(context, residual)?; + state.insert_error(ErrorKind::NoInstanceFound { constraint }); } - let equation_set = equations::analyse_equation_set( - state, - context, - equations::EquationMode::Check { - origin: equations::EquationTypeOrigin::Explicit(signature_id), - expected_type: signature_member_type, - }, - &member.equations, - )?; - let exhaustiveness = equations::compute_equation_exhaustiveness( - state, - context, - &equation_set, - &member.equations, - )?; - state.report_exhaustiveness(context, exhaustiveness); - state.solve_constraints(context)? - } else if let Some(specialised_type) = class_member_type { - let equation_set = equations::analyse_equation_set( - state, - context, - equations::EquationMode::Check { - origin: equations::EquationTypeOrigin::Implicit, - expected_type: specialised_type, - }, - &member.equations, - )?; - let exhaustiveness = equations::compute_equation_exhaustiveness( - state, - context, - &equation_set, - &member.equations, - )?; - state.report_exhaustiveness(context, exhaustiveness); - state.solve_constraints(context)? - } else { - vec![] - }; + Ok(()) + }) +} + +struct FreshenedInstanceRigids { + constraints: Vec, + arguments: Vec, + substitution: NameToType, +} + +fn freshen_instance_rigids( + state: &mut CheckState, + context: &CheckContext, + instance: &toolkit::DecomposedInstance, +) -> QueryResult +where + Q: ExternalQueries, +{ + let mut substitution = NameToType::default(); - for residual in residuals { - let constraint = state.pretty_id(context, residual)?; - state.insert_error(ErrorKind::NoInstanceFound { constraint }); + for binder in &instance.binders { + let kind = SubstituteName::many(state, context, &substitution, binder.kind)?; + let rigid = state.fresh_rigid(context.queries, kind); + substitution.insert(binder.name, rigid); } - Ok(()) + let constraints = instance + .constraints + .iter() + .map(|&constraint| SubstituteName::many(state, context, &substitution, constraint)) + .collect::>>()?; + + let arguments = instance + .arguments + .iter() + .map(|&argument| SubstituteName::many(state, context, &substitution, argument)) + .collect::>>()?; + + Ok(FreshenedInstanceRigids { constraints, arguments, substitution }) } fn specialise_class_member_type( diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index 857f075a..b48a6869 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -11,6 +11,7 @@ use crate::context::CheckContext; use crate::core::exhaustive::{ ExhaustivenessReport, Pattern, PatternConstructor, PatternId, PatternInterner, PatternKind, }; +use crate::core::substitute::{NameToType, SubstituteName}; use crate::core::{Depth, Name, SmolStrId, Type, TypeId, constraint, pretty, zonk}; use crate::error::{CheckError, ErrorCrumb, ErrorKind}; use crate::implication::Implications; @@ -84,18 +85,25 @@ impl Unifications { /// Tracks type variable bindings during kind inference. #[derive(Default)] pub struct Bindings { - forall_bindings: FxHashMap, - implicit_bindings: - FxHashMap<(lowering::GraphNodeId, lowering::ImplicitBindingId), (Name, TypeId)>, + forall: FxHashMap, + implicit: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ImplicitBinding { + node: lowering::GraphNodeId, + id: lowering::ImplicitBindingId, + name: Name, + kind: TypeId, } impl Bindings { pub fn bind_forall(&mut self, id: lowering::TypeVariableBindingId, name: Name, kind: TypeId) { - self.forall_bindings.insert(id, (name, kind)); + self.forall.insert(id, (name, kind)); } pub fn lookup_forall(&self, id: lowering::TypeVariableBindingId) -> Option<(Name, TypeId)> { - self.forall_bindings.get(&id).copied() + self.forall.get(&id).copied() } pub fn bind_implicit( @@ -105,7 +113,32 @@ impl Bindings { name: Name, kind: TypeId, ) { - self.implicit_bindings.insert((node, id), (name, kind)); + self.implicit.push(ImplicitBinding { node, id, name, kind }); + } + + fn bind_implicit_substitution( + state: &mut CheckState, + context: &CheckContext, + substitution: &NameToType, + ) -> QueryResult<()> + where + Q: ExternalQueries, + { + let scope = state.bindings.implicit.len(); + + for binding in 0..scope { + let ImplicitBinding { node, id, name, kind } = state.bindings.implicit[binding]; + let Some(&replacement) = substitution.get(&name) else { continue }; + + let Type::Rigid(name, _, _) = context.lookup_type(replacement) else { + unreachable!("invariant violated: expected a rigid variable"); + }; + + let kind = SubstituteName::many(state, context, substitution, kind)?; + state.bindings.implicit.push(ImplicitBinding { node, id, name, kind }); + } + + Ok(()) } pub fn lookup_implicit( @@ -113,7 +146,11 @@ impl Bindings { node: lowering::GraphNodeId, id: lowering::ImplicitBindingId, ) -> Option<(Name, TypeId)> { - self.implicit_bindings.get(&(node, id)).copied() + self.implicit + .iter() + .rev() + .find(|binding| binding.node == node && binding.id == id) + .map(|binding| (binding.name, binding.kind)) } } @@ -199,6 +236,23 @@ impl CheckState { result } + pub fn with_implicit( + &mut self, + context: &CheckContext, + substitution: &NameToType, + f: impl FnOnce(&mut CheckState) -> QueryResult, + ) -> QueryResult + where + Q: ExternalQueries, + { + let scope = self.bindings.implicit.len(); + Bindings::bind_implicit_substitution(self, context, substitution)?; + let result = f(self); + self.bindings.implicit.truncate(scope); + + result + } + pub fn solve_constraints(&mut self, context: &CheckContext) -> QueryResult> where Q: ExternalQueries, diff --git a/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap index c6a20a62..4df934ed 100644 --- a/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap +++ b/tests-integration/fixtures/checking2/228_instance_implicit_variable_freshening/Main.snap @@ -20,101 +20,3 @@ instance forall (t4 :: Type) (t3 :: (t4 :: Type)). Top (Proxy @(t4 :: Type) (t3 Roles Proxy = [Phantom] - -Errors -CheckError { - kind: CannotUnify { - t1: Id(6), - t2: Id(7), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - InferringKind( - AstId(34), - ), - CheckingKind( - AstId(36), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(9), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - CheckingExpression( - AstId(33), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(10), - t2: Id(11), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - CheckingExpression( - AstId(33), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(6), - t2: Id(7), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(12), - t2: Id(13), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(11), - t2: Id(14), - }, - crumbs: [ - TermDeclaration( - Idx::(2), - ), - CheckingExpression( - AstId(32), - ), - ], -} From 3deb4d49d3d6316a3666535d3d7c1eb4b600e945 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 11 Mar 2026 05:09:55 +0800 Subject: [PATCH 382/386] Defer expansion when checking synonym arguments --- compiler-core/checking2/src/source/operator.rs | 2 +- compiler-core/checking2/src/source/synonym.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index e602b8b7..a54a79f6 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -357,7 +357,7 @@ impl IsOperator for lowering::TypeId { let operator_kind = toolkit::lookup_file_type(state, context, file_id, item_id)?; if let Some((elaborated_type, result_kind)) = - synonym::try_check_resolved_synonym_application( + synonym::try_check_synonym_application( state, context, (target_file_id, target_item_id), diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index 06d5c60f..f6b444dd 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -151,10 +151,14 @@ where .map(|(type_id, kind_id)| Argument::Core(type_id, kind_id)) .collect_vec(); - check_synonym_application_arguments(state, context, synonym, &arguments) + let defer_expansion = mem::replace(&mut state.defer_expansion, true); + let checked = check_synonym_application_arguments(state, context, synonym, &arguments); + state.defer_expansion = defer_expansion; + + checked } -pub fn try_check_resolved_synonym_application( +pub fn try_check_synonym_application( state: &mut CheckState, context: &CheckContext, (file_id, type_id): (FileId, TypeItemId), From a5ccf8bc8a80231a1e02704c219ddb4de23d0e02 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 11 Mar 2026 14:00:46 +0800 Subject: [PATCH 383/386] Add test case for partial synonym error in operators --- .../Main.purs | 18 ++++++ .../Main.snap | 58 +++++++++++++++++++ .../tests/checking2/generated.rs | 2 + 3 files changed, 78 insertions(+) create mode 100644 tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.purs create mode 100644 tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap diff --git a/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.purs b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.purs new file mode 100644 index 00000000..67ba5ac1 --- /dev/null +++ b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.purs @@ -0,0 +1,18 @@ +module Main where + +type RowApply :: forall k. (Row k -> Row k) -> Row k -> Row k +type RowApply f a = f a + +infixr 0 type RowApply as + + +type AddField :: Type -> Row Type -> Row Type +type AddField a r = + ( field :: a + | r + ) + +type Bad :: Type -> Row Type +type Bad a = AddField a + () + +type Good :: Type -> Row Type +type Good a = RowApply (AddField a) () diff --git a/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap new file mode 100644 index 00000000..0fa3c95f --- /dev/null +++ b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap @@ -0,0 +1,58 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms + +Types +RowApply :: + forall (k :: Type). (Row (k :: Type) -> Row (k :: Type)) -> Row (k :: Type) -> Row (k :: Type) ++ :: forall (k :: Type). (Row (k :: Type) -> Row (k :: Type)) -> Row (k :: Type) -> Row (k :: Type) +AddField :: Type -> Row Type -> Row Type +Bad :: Type -> Row Type +Good :: Type -> Row Type + +Synonyms +type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Type)) +type AddField a r = ( field :: (a :: Type) | (r :: Row Type) ) +type Bad a = RowApply @Type ?[partial synonym application] () +type Good a = RowApply @Type (AddField (a :: Type)) () + +Errors +CheckError { + kind: PartialSynonymApplication { + id: AstId(58), + }, + crumbs: [ + CheckingKind( + AstId(57), + ), + InferringKind( + AstId(57), + ), + CheckingKind( + AstId(58), + ), + InferringKind( + AstId(58), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + CheckingKind( + AstId(57), + ), + InferringKind( + AstId(57), + ), + CheckingKind( + AstId(58), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 57db3efb..3474a737 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -481,3 +481,5 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_227_derive_newtype_invalid_skolem_layout_main() { run_test("227_derive_newtype_invalid_skolem_layout", "Main"); } #[rustfmt::skip] #[test] fn test_228_instance_implicit_variable_freshening_main() { run_test("228_instance_implicit_variable_freshening", "Main"); } + +#[rustfmt::skip] #[test] fn test_229_type_operator_synonym_partial_arguments_main() { run_test("229_type_operator_synonym_partial_arguments", "Main"); } From 9de1a7a11eda7837bcc3de0b3ab218c940694234 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Wed, 11 Mar 2026 05:19:47 +0800 Subject: [PATCH 384/386] Defer expansion when checking operator arguments --- .../checking2/src/source/operator.rs | 77 +++++++++++++------ compiler-core/checking2/src/source/synonym.rs | 18 ++--- compiler-core/checking2/src/state.rs | 7 ++ .../Main.snap | 40 +--------- 4 files changed, 67 insertions(+), 75 deletions(-) diff --git a/compiler-core/checking2/src/source/operator.rs b/compiler-core/checking2/src/source/operator.rs index a54a79f6..a39b8a89 100644 --- a/compiler-core/checking2/src/source/operator.rs +++ b/compiler-core/checking2/src/source/operator.rs @@ -144,21 +144,31 @@ where let _ = unification::subtype(state, context, result_type, expected_type)?; } - let [left_tree, right_tree] = children; - - let (left, _) = traverse_operator_tree( - state, - context, - left_tree, - OperatorKindMode::Check { expected_type: left_type }, - )?; - - let (right, _) = traverse_operator_tree( - state, - context, - right_tree, - OperatorKindMode::Check { expected_type: right_type }, - )?; + let check_left_right = |state: &mut CheckState| { + let [left_tree, right_tree] = children; + + let (left, _) = traverse_operator_tree( + state, + context, + left_tree, + OperatorKindMode::Check { expected_type: left_type }, + )?; + + let (right, _) = traverse_operator_tree( + state, + context, + right_tree, + OperatorKindMode::Check { expected_type: right_type }, + )?; + + Ok((left, right)) + }; + + let (left, right) = if E::should_defer_expansion(state, context, operator)? { + state.with_defer_expansion(check_left_right)? + } else { + check_left_right(state)? + }; E::build(state, context, operator, (left, right), (left_type, right_type), result_type) } @@ -199,6 +209,14 @@ pub trait IsOperator: IsElement { expected: TypeId, ) -> QueryResult<(Self::Elaborated, TypeId)>; + fn should_defer_expansion( + _state: &CheckState, + _context: &CheckContext, + _operator: (FileId, Self::ItemId), + ) -> QueryResult { + Ok(false) + } + fn build( state: &mut CheckState, context: &CheckContext, @@ -339,6 +357,19 @@ impl IsOperator for lowering::TypeId { types::check_kind(state, context, id, expected) } + fn should_defer_expansion( + state: &CheckState, + context: &CheckContext, + (file_id, item_id): (FileId, Self::ItemId), + ) -> QueryResult { + let Some((target_file_id, target_item_id)) = + toolkit::resolve_type_operator_target(context, file_id, item_id)? + else { + return Ok(false); + }; + Ok(toolkit::lookup_file_synonym(state, context, target_file_id, target_item_id)?.is_some()) + } + fn build( state: &mut CheckState, context: &CheckContext, @@ -356,15 +387,13 @@ impl IsOperator for lowering::TypeId { let operator_kind = toolkit::lookup_file_type(state, context, file_id, item_id)?; - if let Some((elaborated_type, result_kind)) = - synonym::try_check_synonym_application( - state, - context, - (target_file_id, target_item_id), - operator_kind, - &[(left, left_kind), (right, right_kind)], - )? - { + if let Some((elaborated_type, result_kind)) = synonym::try_check_synonym_application( + state, + context, + (target_file_id, target_item_id), + operator_kind, + &[(left, left_kind), (right, right_kind)], + )? { let result_kind = normalise::normalise(state, context, result_kind)?; return Ok((elaborated_type, result_kind)); } diff --git a/compiler-core/checking2/src/source/synonym.rs b/compiler-core/checking2/src/source/synonym.rs index f6b444dd..0d13ebf1 100644 --- a/compiler-core/checking2/src/source/synonym.rs +++ b/compiler-core/checking2/src/source/synonym.rs @@ -1,6 +1,4 @@ //! Implements syntax-driven checking rules for synonym detection. - -use std::mem; use std::ops::ControlFlow; use std::sync::Arc; @@ -129,11 +127,9 @@ where let arguments = arguments.iter().copied().map(Argument::Syntax).collect_vec(); - let defer_expansion = mem::replace(&mut state.defer_expansion, true); - let checked = check_synonym_application_arguments(state, context, synonym, &arguments); - state.defer_expansion = defer_expansion; - - checked + state.with_defer_expansion(|state| { + check_synonym_application_arguments(state, context, synonym, &arguments) + }) } pub fn check_synonym_application( @@ -151,11 +147,9 @@ where .map(|(type_id, kind_id)| Argument::Core(type_id, kind_id)) .collect_vec(); - let defer_expansion = mem::replace(&mut state.defer_expansion, true); - let checked = check_synonym_application_arguments(state, context, synonym, &arguments); - state.defer_expansion = defer_expansion; - - checked + state.with_defer_expansion(|state| { + check_synonym_application_arguments(state, context, synonym, &arguments) + }) } pub fn try_check_synonym_application( diff --git a/compiler-core/checking2/src/state.rs b/compiler-core/checking2/src/state.rs index b48a6869..383f1679 100644 --- a/compiler-core/checking2/src/state.rs +++ b/compiler-core/checking2/src/state.rs @@ -196,6 +196,13 @@ impl CheckState { result } + pub fn with_defer_expansion(&mut self, f: impl FnOnce(&mut CheckState) -> T) -> T { + let previous = mem::replace(&mut self.defer_expansion, true); + let result = f(self); + self.defer_expansion = previous; + result + } + pub fn with_error_crumb(&mut self, crumb: ErrorCrumb, f: F) -> T where F: FnOnce(&mut CheckState) -> T, diff --git a/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap index 0fa3c95f..3f35256a 100644 --- a/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap +++ b/tests-integration/fixtures/checking2/229_type_operator_synonym_partial_arguments/Main.snap @@ -16,43 +16,5 @@ Good :: Type -> Row Type Synonyms type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Type)) type AddField a r = ( field :: (a :: Type) | (r :: Row Type) ) -type Bad a = RowApply @Type ?[partial synonym application] () +type Bad a = RowApply @Type (AddField (a :: Type)) () type Good a = RowApply @Type (AddField (a :: Type)) () - -Errors -CheckError { - kind: PartialSynonymApplication { - id: AstId(58), - }, - crumbs: [ - CheckingKind( - AstId(57), - ), - InferringKind( - AstId(57), - ), - CheckingKind( - AstId(58), - ), - InferringKind( - AstId(58), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(10), - t2: Id(11), - }, - crumbs: [ - CheckingKind( - AstId(57), - ), - InferringKind( - AstId(57), - ), - CheckingKind( - AstId(58), - ), - ], -} From bb4cab4a85b77992f05a3ab5d6950c52d6b7b368 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Mar 2026 19:23:30 +0800 Subject: [PATCH 385/386] Add tests for higher-order synonyms as arguments --- .../230_type_synonym_higher_order/Main.purs | 16 ++++++ .../230_type_synonym_higher_order/Main.snap | 50 ++++++++++++++++++ .../Main.purs | 18 +++++++ .../Main.snap | 51 +++++++++++++++++++ .../tests/checking2/generated.rs | 4 ++ 5 files changed, 139 insertions(+) create mode 100644 tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.purs create mode 100644 tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap create mode 100644 tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.purs create mode 100644 tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap diff --git a/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.purs b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.purs new file mode 100644 index 00000000..8ffb9753 --- /dev/null +++ b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.purs @@ -0,0 +1,16 @@ +module Main where + +type RowApply :: forall k. (Row k -> Row k) -> Row k -> Row k +type RowApply f a = f a + +type AddInt :: Row Type -> Row Type +type AddInt r = ( int :: Int | r ) + +type AddString :: Row Type -> Row Type +type AddString r = ( string :: String | r ) + +type Test :: Row Type +type Test = RowApply AddInt (RowApply AddString ()) + +test :: Record Test +test = { int: 42, string: "life" } diff --git a/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap new file mode 100644 index 00000000..cc1b17cd --- /dev/null +++ b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap @@ -0,0 +1,50 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: {| Test } + +Types +RowApply :: + forall (k :: Type). (Row (k :: Type) -> Row (k :: Type)) -> Row (k :: Type) -> Row (k :: Type) +AddInt :: Row Type -> Row Type +AddString :: Row Type -> Row Type +Test :: Row Type + +Synonyms +type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Type)) +type AddInt r = ( int :: Int | (r :: Row Type) ) +type AddString r = ( string :: String | (r :: Row Type) ) +type Test = RowApply @Type AddInt (RowApply @Type AddString ()) + +Errors +CheckError { + kind: CannotUnify { + t1: Id(8), + t2: Id(9), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(80), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(80), + ), + ], +} diff --git a/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.purs b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.purs new file mode 100644 index 00000000..fe98b70d --- /dev/null +++ b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.purs @@ -0,0 +1,18 @@ +module Main where + +type RowApply :: forall k. (Row k -> Row k) -> Row k -> Row k +type RowApply f a = f a + +infixr 0 type RowApply as + + +type AddInt :: Row Type -> Row Type +type AddInt r = ( int :: Int | r ) + +type AddString :: Row Type -> Row Type +type AddString r = ( string :: String | r ) + +type Test :: Row Type +type Test = AddInt + AddString + () + +test :: Record Test +test = { int: 42, string: "life" } diff --git a/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap new file mode 100644 index 00000000..bf54e5e0 --- /dev/null +++ b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap @@ -0,0 +1,51 @@ +--- +source: tests-integration/tests/checking2/generated.rs +assertion_line: 28 +expression: report +--- +Terms +test :: {| Test } + +Types +RowApply :: + forall (k :: Type). (Row (k :: Type) -> Row (k :: Type)) -> Row (k :: Type) -> Row (k :: Type) ++ :: forall (k :: Type). (Row (k :: Type) -> Row (k :: Type)) -> Row (k :: Type) -> Row (k :: Type) +AddInt :: Row Type -> Row Type +AddString :: Row Type -> Row Type +Test :: Row Type + +Synonyms +type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Type)) +type AddInt r = ( int :: Int | (r :: Row Type) ) +type AddString r = ( string :: String | (r :: Row Type) ) +type Test = RowApply @Type AddInt (RowApply @Type AddString ()) + +Errors +CheckError { + kind: CannotUnify { + t1: Id(10), + t2: Id(11), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(81), + ), + ], +} +CheckError { + kind: CannotUnify { + t1: Id(12), + t2: Id(13), + }, + crumbs: [ + TermDeclaration( + Idx::(0), + ), + CheckingExpression( + AstId(81), + ), + ], +} diff --git a/tests-integration/tests/checking2/generated.rs b/tests-integration/tests/checking2/generated.rs index 3474a737..c21e0e4b 100644 --- a/tests-integration/tests/checking2/generated.rs +++ b/tests-integration/tests/checking2/generated.rs @@ -483,3 +483,7 @@ fn run_test(folder: &str, file: &str) { #[rustfmt::skip] #[test] fn test_228_instance_implicit_variable_freshening_main() { run_test("228_instance_implicit_variable_freshening", "Main"); } #[rustfmt::skip] #[test] fn test_229_type_operator_synonym_partial_arguments_main() { run_test("229_type_operator_synonym_partial_arguments", "Main"); } + +#[rustfmt::skip] #[test] fn test_230_type_synonym_higher_order_main() { run_test("230_type_synonym_higher_order", "Main"); } + +#[rustfmt::skip] #[test] fn test_231_type_synonym_higher_order_operator_main() { run_test("231_type_synonym_higher_order_operator", "Main"); } From 1a2ce3776c630cf2c28899273e3f39bb8f7a907f Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Thu, 12 Mar 2026 19:27:22 +0800 Subject: [PATCH 386/386] Fix expansion for discovered synonym applications --- compiler-core/checking2/src/core/normalise.rs | 33 +++++++++++++------ .../230_type_synonym_higher_order/Main.snap | 30 ----------------- .../Main.snap | 30 ----------------- 3 files changed, 23 insertions(+), 70 deletions(-) diff --git a/compiler-core/checking2/src/core/normalise.rs b/compiler-core/checking2/src/core/normalise.rs index 7c2326be..7fa7e61a 100644 --- a/compiler-core/checking2/src/core/normalise.rs +++ b/compiler-core/checking2/src/core/normalise.rs @@ -1,13 +1,14 @@ //! Implements normalisation algorithms for the core representation. use std::iter; +use std::sync::Arc; use building_types::QueryResult; use itertools::Itertools; use crate::context::CheckContext; use crate::core::substitute::{NameToType, SubstituteName}; -use crate::core::{KindOrType, RowType, Saturation, Type, TypeId, toolkit}; +use crate::core::{KindOrType, RowType, Saturation, Synonym, Type, TypeId, toolkit}; use crate::state::{CheckState, UnificationState}; use crate::{ExternalQueries, safe_loop}; @@ -187,6 +188,10 @@ where // Collect oversaturated arguments, in our example above, this // would collect `[Int]` and `[Int, String]` into `arguments`. + // Additionally, these are used for Constructor-based synonyms, + // which occur since we don't normalise applications back into + // the SynonymApplication form. This will be rendered obsolete + // once these variants are unified. safe_loop! { current = normalise(state, context, current)?; match context.lookup_type(current) { @@ -203,16 +208,24 @@ where } current = normalise(state, context, current)?; - let Type::SynonymApplication(synonym_id) = context.lookup_type(current) else { - return Ok(id); - }; + let ((file_id, type_id), synonym_arguments) = match context.lookup_type(current) { + Type::Constructor(file_id, type_id) => { + let reference = (file_id, type_id); + let arguments = [].into(); + (reference, arguments) + } + Type::SynonymApplication(synonym_id) => { + let Synonym { saturation, reference, arguments } = context.lookup_synonym(synonym_id); - let synonym_application = context.lookup_synonym(synonym_id); - if synonym_application.saturation != Saturation::Full { - return Ok(id); - } + if saturation != Saturation::Full { + return Ok(id); + } + + (reference, Arc::clone(&arguments)) + } + _ => return Ok(id), + }; - let (file_id, type_id) = synonym_application.reference; let checked_synonym = toolkit::lookup_file_synonym(state, context, file_id, type_id)?; let Some(checked_synonym) = checked_synonym else { return Ok(id); @@ -221,7 +234,7 @@ where let mut bindings = NameToType::default(); let mut kind = checked_synonym.kind; let mut arguments = { - let synonym_arguments = synonym_application.arguments.iter().copied(); + let synonym_arguments = synonym_arguments.iter().copied(); let applied_arguments = applied_arguments.iter().copied().rev(); iter::chain(synonym_arguments, applied_arguments) }; diff --git a/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap index cc1b17cd..21bcc581 100644 --- a/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap +++ b/tests-integration/fixtures/checking2/230_type_synonym_higher_order/Main.snap @@ -18,33 +18,3 @@ type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Ty type AddInt r = ( int :: Int | (r :: Row Type) ) type AddString r = ( string :: String | (r :: Row Type) ) type Test = RowApply @Type AddInt (RowApply @Type AddString ()) - -Errors -CheckError { - kind: CannotUnify { - t1: Id(8), - t2: Id(9), - }, - crumbs: [ - TermDeclaration( - Idx::(0), - ), - CheckingExpression( - AstId(80), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(10), - t2: Id(11), - }, - crumbs: [ - TermDeclaration( - Idx::(0), - ), - CheckingExpression( - AstId(80), - ), - ], -} diff --git a/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap index bf54e5e0..7060e509 100644 --- a/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap +++ b/tests-integration/fixtures/checking2/231_type_synonym_higher_order_operator/Main.snap @@ -19,33 +19,3 @@ type RowApply f a = (f :: Row (k :: Type) -> Row (k :: Type)) (a :: Row (k :: Ty type AddInt r = ( int :: Int | (r :: Row Type) ) type AddString r = ( string :: String | (r :: Row Type) ) type Test = RowApply @Type AddInt (RowApply @Type AddString ()) - -Errors -CheckError { - kind: CannotUnify { - t1: Id(10), - t2: Id(11), - }, - crumbs: [ - TermDeclaration( - Idx::(0), - ), - CheckingExpression( - AstId(81), - ), - ], -} -CheckError { - kind: CannotUnify { - t1: Id(12), - t2: Id(13), - }, - crumbs: [ - TermDeclaration( - Idx::(0), - ), - CheckingExpression( - AstId(81), - ), - ], -}