Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions source/compiler/qsc_frontend/src/typeck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//! For example, a rule might say that if a statement is an expression, it must
//! return `Unit`. The inferrer would then be used to get the inferred type out of
//! the expression, giving us a type id, which we can then constrain to `Unit`.
//!
//! [`validate_instantiation`] reuses the same solver for already-concrete HIR generic
//! arguments, without source inference or defaulting.
#![allow(unused_assignments)]
// clippy false positive bug: https://github.com/rust-lang/rust/issues/147648. Remove when fixed.
mod check;
Expand All @@ -22,7 +25,7 @@ use qsc_ast::ast::NodeId;
use qsc_data_structures::{index_map::IndexMap, span::Span};
use qsc_hir::{
hir::{CallableKind, ItemId},
ty::{FunctorSet, GenericArg, InferTyId, Prim, Ty, Udt},
ty::{FunctorSet, GenericArg, InferTyId, ParamId, Prim, Ty, TypeParameter, Udt},
};
use rustc_hash::FxHashMap;
use std::fmt::Debug;
Expand All @@ -42,10 +45,57 @@ pub struct Table {
pub generics: IndexMap<NodeId, Vec<GenericArg>>,
}

/// A source type-checking diagnostic.
#[derive(Clone, Debug, Diagnostic, Error)]
#[diagnostic(transparent)]
#[error(transparent)]
pub(super) struct Error(ErrorKind);
pub struct Error(ErrorKind);

/// A rejected concrete generic instantiation, with the responsible parameter when known.
#[derive(Clone, Debug, Diagnostic, Error)]
#[diagnostic(forward(error))]
#[error("{error}")]
pub struct InstantiationError {
/// The parameter whose candidate or bounds failed, absent for arity errors.
pub parameter: Option<ParamId>,
/// The underlying diagnostic, including type/bound details and source location.
#[source]
pub error: Error,
}

/// Validates concrete generic arguments using the source type checker's constraint solver.
///
/// Parameter IDs may be sparse and include inherited parameters not declared by a callable.
/// IDs must be unique, and `candidates` must contain exactly those IDs with matching argument
/// kinds. Candidates must already be concrete; this function neither infers nor defaults
/// them. Bound payloads such as `Exp['P]` are substituted using the complete candidate map
/// before checking, so every parameter referenced by a payload must also be supplied.
///
/// Nominal types, including those reachable through other nominal definitions, require
/// their definitions in `udts`, keyed by fully qualified item identity. Their representation
/// is checked for concreteness without erasing the identity used by class constraints.
/// The solver operates on private copies of candidate types; callers may retain read
/// borrows of arrow fields in the supplied candidates and bound payloads.
///
/// # Errors
///
/// Returns argument-shape, incomplete-type, or source class/functor diagnostics at `span`.
/// Invalid candidates are rejected before bounds are checked. Bound errors identify the
/// parameter declaring the bound, even when a substituted payload caused the failure.
/// Success means every obligation, including recursively derived obligations, was discharged;
/// suspended constraints and solver recursion limits are reported as unresolved constraints.
#[allow(
clippy::implicit_hasher,
reason = "Uses the frontend's semantic type table and solver representation."
)]
pub fn validate_instantiation(
parameters: &[(ParamId, TypeParameter)],
candidates: &FxHashMap<ParamId, GenericArg>,
udts: &FxHashMap<ItemId, Udt>,
span: Span,
) -> Result<(), Vec<InstantiationError>> {
infer::validate_instantiation(parameters, candidates, udts, span)
}

/// Simplified type info for error reporting. Same shape as `Ty`, but without `Rc`
/// so it can be included in `ErrorKind` (which must be `Send + Sync`).
Expand Down Expand Up @@ -113,6 +163,30 @@ impl From<Ty> for TyInfo {

#[derive(Clone, Debug, Diagnostic, Error)]
enum ErrorKind {
#[error("expected {0} generic arguments, found {1}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.InstantiationArity"))]
InstantiationArity(usize, usize, #[label] Span),
#[error("generic parameter {0} is declared more than once")]
#[diagnostic(code("Qdk.Qsc.TypeCk.DuplicateParameter"))]
DuplicateParameter(ParamId, #[label] Span),
#[error("no candidate was supplied for generic parameter {0}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.MissingArgument"))]
MissingArgument(ParamId, #[label] Span),
#[error("generic argument has the wrong kind for parameter {0}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.InstantiationKind"))]
InstantiationKind(ParamId, #[label] Span),
#[error("expected a concrete type, found {0}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.NonConcreteType"))]
NonConcreteType(TyInfo, #[label] Span),
#[error("expected concrete functors, found {0}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.NonConcreteFunctor"))]
NonConcreteFunctor(FunctorSet, #[label] Span),
#[error("missing definition for nominal type {0}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.MissingTypeDefinition"))]
MissingTypeDefinition(ItemId, #[label] Span),
#[error("generic constraints were not fully discharged")]
#[diagnostic(code("Qdk.Qsc.TypeCk.UnresolvedConstraints"))]
UnresolvedConstraints(#[label] Span),
#[error("expected {0}, found {1}")]
#[diagnostic(code("Qdk.Qsc.TypeCk.TyMismatch"))]
TyMismatch(
Expand Down
191 changes: 184 additions & 7 deletions source/compiler/qsc_frontend/src/typeck/infer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use super::{Error, ErrorKind};
use super::{Error, ErrorKind, InstantiationError};
use qsc_data_structures::{index_map::IndexMap, span::Span};
use qsc_hir::{
hir::{ItemId, PrimField, Res},
ty::{
Arrow, ClassConstraint, FunctorSet, FunctorSetValue, GenericArg, InferFunctorId, InferTyId,
Prim, Scheme, Ty, TypeParameter, Udt,
ParamId, Prim, Scheme, Ty, TypeParameter, Udt, instantiate_ty,
},
};
use rustc_hash::{FxHashMap, FxHashSet};
Expand All @@ -21,6 +21,174 @@ use std::{
const MAX_TY_RECURSION_DEPTH: i8 = 100;
const MAX_TY_SIZE: usize = 100;

pub(super) fn validate_instantiation(
parameters: &[(ParamId, TypeParameter)],
candidates: &FxHashMap<ParamId, GenericArg>,
udts: &FxHashMap<ItemId, Udt>,
span: Span,
) -> Result<(), Vec<InstantiationError>> {
let mut errors = Vec::new();
if parameters.len() != candidates.len() {
errors.push(InstantiationError {
parameter: None,
error: Error(ErrorKind::InstantiationArity(
parameters.len(),
candidates.len(),
span,
)),
});
}
let mut seen = FxHashSet::default();
// Solver substitution mutably borrows arrow fields, even in concrete types.
// Keep its copies independent of any read borrows held by the caller.
let mut concrete_candidates = FxHashMap::default();
for (id, parameter) in parameters {
let candidate = if seen.insert(*id) {
match (parameter, candidates.get(id)) {
(_, None) => Err(ErrorKind::MissingArgument(*id, span)),
(TypeParameter::Ty { .. }, Some(GenericArg::Ty(ty))) => {
instantiate_concrete_ty(ty, &FxHashMap::default(), udts, span)
.map(GenericArg::Ty)
}
(TypeParameter::Functor(_), Some(GenericArg::Functor(functors))) => {
concrete_functors(*functors, span).map(|()| GenericArg::Functor(*functors))
}
_ => Err(ErrorKind::InstantiationKind(*id, span)),
}
} else {
Err(ErrorKind::DuplicateParameter(*id, span))
};
match candidate {
Ok(candidate) => {
concrete_candidates.insert(*id, candidate);
}
Err(error) => errors.push(InstantiationError {
parameter: Some(*id),
error: Error(error),
}),
}
}
if !errors.is_empty() {
return Err(errors);
}

for (id, parameter) in parameters {
let mut constraints = VecDeque::new();
match (parameter, &concrete_candidates[id]) {
(TypeParameter::Ty { bounds, .. }, GenericArg::Ty(ty)) => {
for bound in &bounds.0 {
match instantiate_bound(bound, &concrete_candidates, udts, span) {
Ok(bound) => {
constraints.push_back(into_constraint(ty.clone(), &bound, span));
}
Err(error) => errors.push(InstantiationError {
parameter: Some(*id),
error: Error(error),
}),
}
}
}
(TypeParameter::Functor(expected), GenericArg::Functor(actual)) => {
constraints.push_back(Constraint::Superset {
expected: *expected,
actual: *actual,
span,
});
}
_ => unreachable!("argument kinds were checked"),
}
// Solve each parameter separately to retain ownership of derived diagnostics.
// Unlike inference, validation cannot default or leave obligations unresolved.
let mut solver = Solver::new();
solver.solve(udts, &mut constraints);
if solver.incomplete
|| !solver.pending_tys.is_empty()
|| !solver.pending_functors.is_empty()
{
solver
.errors
.push(Error(ErrorKind::UnresolvedConstraints(span)));
}
errors.extend(solver.errors.into_iter().map(|error| InstantiationError {
parameter: Some(*id),
error,
}));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}

fn instantiate_bound(
bound: &ClassConstraint,
candidates: &FxHashMap<ParamId, GenericArg>,
udts: &FxHashMap<ItemId, Udt>,
span: Span,
) -> Result<ClassConstraint, ErrorKind> {
let mut bound = bound.clone();
if let ClassConstraint::Exp { power: ty } | ClassConstraint::Iterable { item: ty } = &mut bound
{
*ty = instantiate_concrete_ty(ty, candidates, udts, span)?;
}
Ok(bound)
}

fn instantiate_concrete_ty(
ty: &Ty,
candidates: &FxHashMap<ParamId, GenericArg>,
udts: &FxHashMap<ItemId, Udt>,
span: Span,
) -> Result<Ty, ErrorKind> {
let ty = instantiate_ty(|id| candidates.get(id), ty).map_err(|error| match error {
qsc_hir::ty::InstantiationError::Kind(id) => ErrorKind::InstantiationKind(id, span),
_ => unreachable!("type substitution only checks argument kinds"),
})?;
concrete_ty(&ty, udts, &mut FxHashSet::default(), span)?;
Ok(ty)
}

fn concrete_functors(functors: FunctorSet, span: Span) -> Result<(), ErrorKind> {
if matches!(functors, FunctorSet::Value(_)) {
Ok(())
} else {
Err(ErrorKind::NonConcreteFunctor(functors, span))
}
}

fn concrete_ty(
ty: &Ty,
udts: &FxHashMap<ItemId, Udt>,
visited: &mut FxHashSet<ItemId>,
span: Span,
) -> Result<(), ErrorKind> {
match ty {
Ty::Prim(_) => Ok(()),
Ty::Array(item) => concrete_ty(item, udts, visited, span),
Ty::Tuple(items) => items
.iter()
.try_for_each(|item| concrete_ty(item, udts, visited, span)),
Ty::Arrow(arrow) => {
concrete_ty(&arrow.input.borrow(), udts, visited, span)?;
concrete_ty(&arrow.output.borrow(), udts, visited, span)?;
concrete_functors(*arrow.functors.borrow(), span)
}
Ty::Udt(_, Res::Item(id)) => {
let udt = udts
.get(id)
.ok_or(ErrorKind::MissingTypeDefinition(*id, span))?;
if visited.insert(*id) {
concrete_ty(&udt.get_pure_ty(), udts, visited, span)?;
}
Ok(())
}
Ty::Param { .. } | Ty::Infer(_) | Ty::Err | Ty::Udt(_, _) => {
Err(ErrorKind::NonConcreteType(ty.into(), span))
}
}
}

#[derive(Debug, Default)]
struct Solution {
tys: IndexMap<InferTyId, Ty>,
Expand Down Expand Up @@ -535,11 +703,7 @@ impl Inferrer {

/// Solves for all variables given the accumulated constraints.
pub(super) fn solve(&mut self, udts: &FxHashMap<ItemId, Udt>) -> Vec<Error> {
while let Some(constraint) = self.constraints.pop_front() {
for constraint in self.solver.constrain(udts, constraint).into_iter().rev() {
self.constraints.push_front(constraint);
}
}
self.solver.solve(udts, &mut self.constraints);
let unresolved_ty_errs = self.find_unresolved_types();
self.solver.default_functors(self.next_functor);
self.solver
Expand Down Expand Up @@ -590,6 +754,7 @@ struct Solver {
pending_tys: FxHashMap<InferTyId, Vec<Class>>,
pending_functors: FxHashMap<InferFunctorId, FunctorSetValue>,
errors: Vec<Error>,
incomplete: bool,
}

impl Solver {
Expand All @@ -599,6 +764,15 @@ impl Solver {
pending_tys: FxHashMap::default(),
pending_functors: FxHashMap::default(),
errors: Vec::new(),
incomplete: false,
}
}

fn solve(&mut self, udts: &FxHashMap<ItemId, Udt>, constraints: &mut VecDeque<Constraint>) {
while let Some(constraint) = constraints.pop_front() {
for derived in self.constrain(udts, constraint).into_iter().rev() {
constraints.push_front(derived);
}
}
}

Expand Down Expand Up @@ -673,6 +847,9 @@ impl Solver {
{
self.unify(&expected, &actual, span)
} else {
// Source inference diagnoses unresolved variables later. Concrete validation
// must also know that this obligation was not checked.
self.incomplete = true;
Vec::new()
}
}
Expand Down
Loading
Loading