use nova_hir::{SumType, Ty};
use nova_resolver::DefId;
#[derive(Clone, PartialEq, Debug)]
pub enum Ctor {
Variant(DefId, u32),
Bool(bool),
Int(i64),
Str(String),
}
#[derive(Clone, Debug)]
pub enum Pat {
Wild,
Ctor(Ctor, Vec<Pat>),
}
pub struct MatchCx<'a> {
sums: &'a [SumType],
}
type Witness = Vec<Pat>;
impl<'a> MatchCx<'a> {
pub fn new(sums: &'a [SumType]) -> Self {
Self { sums }
}
pub fn usefulness(&self, matrix: &[Vec<Pat>], q: &[Pat], col_tys: &[Ty]) -> Vec<Witness> {
if q.is_empty() {
return if matrix.is_empty() {
vec![Vec::new()]
} else {
Vec::new()
};
}
match &q[0] {
Pat::Ctor(c, args) => {
let spec = self.specialize(matrix, c);
let spec_tys = self.specialized_tys(c, col_tys);
let mut sq = args.clone();
sq.extend_from_slice(&q[1..]);
self.usefulness(&spec, &sq, &spec_tys)
.into_iter()
.map(|w| reassemble(c, w, args.len()))
.collect()
}
Pat::Wild => {
let present = column_ctors(matrix);
if self.is_complete(&col_tys[0], &present) {
let mut out = Vec::new();
for c in self.signature(&col_tys[0]) {
let arity = self.arity(&c);
let spec = self.specialize(matrix, &c);
let spec_tys = self.specialized_tys(&c, col_tys);
let mut sq = vec![Pat::Wild; arity];
sq.extend_from_slice(&q[1..]);
out.extend(
self.usefulness(&spec, &sq, &spec_tys)
.into_iter()
.map(|w| reassemble(&c, w, arity)),
);
}
out
} else {
let def = default_matrix(matrix);
let tail = self.usefulness(&def, &q[1..], &col_tys[1..]);
let heads = self.missing_heads(&col_tys[0], &present);
let mut out = Vec::new();
for w in &tail {
for h in &heads {
let mut row = vec![h.clone()];
row.extend_from_slice(w);
out.push(row);
}
}
out
}
}
}
}
fn specialize(&self, matrix: &[Vec<Pat>], c: &Ctor) -> Vec<Vec<Pat>> {
let arity = self.arity(c);
let mut out = Vec::new();
for row in matrix {
match &row[0] {
Pat::Ctor(rc, rargs) if rc == c => {
let mut nr = rargs.clone();
nr.extend_from_slice(&row[1..]);
out.push(nr);
}
Pat::Ctor(_, _) => {}
Pat::Wild => {
let mut nr = vec![Pat::Wild; arity];
nr.extend_from_slice(&row[1..]);
out.push(nr);
}
}
}
out
}
fn specialized_tys(&self, c: &Ctor, col_tys: &[Ty]) -> Vec<Ty> {
let mut tys = self.arg_tys(c, &col_tys[0]);
tys.extend_from_slice(&col_tys[1..]);
tys
}
fn arg_tys(&self, c: &Ctor, ty0: &Ty) -> Vec<Ty> {
match c {
Ctor::Variant(sum_id, vi) => {
let args = match ty0 {
Ty::Sum { args, .. } => args.clone(),
_ => Vec::new(),
};
self.variant(*sum_id, *vi)
.map(|v| v.fields.iter().map(|f| f.subst(&args)).collect())
.unwrap_or_default()
}
Ctor::Bool(_) | Ctor::Int(_) | Ctor::Str(_) => Vec::new(),
}
}
fn arity(&self, c: &Ctor) -> usize {
match c {
Ctor::Variant(sum_id, vi) => self
.variant(*sum_id, *vi)
.map(|v| v.fields.len())
.unwrap_or(0),
Ctor::Bool(_) | Ctor::Int(_) | Ctor::Str(_) => 0,
}
}
fn signature(&self, ty: &Ty) -> Vec<Ctor> {
match ty {
Ty::Bool => vec![Ctor::Bool(true), Ctor::Bool(false)],
Ty::Sum { def_id, .. } => self
.sums
.iter()
.find(|s| s.def_id == *def_id)
.map(|s| {
(0..s.variants.len() as u32)
.map(|i| Ctor::Variant(*def_id, i))
.collect()
})
.unwrap_or_default(),
_ => Vec::new(),
}
}
fn is_complete(&self, ty: &Ty, present: &[Ctor]) -> bool {
let sig = self.signature(ty);
!sig.is_empty() && sig.iter().all(|c| present.contains(c))
}
fn missing_heads(&self, ty: &Ty, present: &[Ctor]) -> Vec<Pat> {
let sig = self.signature(ty);
if sig.is_empty() {
return vec![Pat::Wild];
}
let missing: Vec<Pat> = sig
.iter()
.filter(|c| !present.contains(c))
.map(|c| Pat::Ctor(c.clone(), vec![Pat::Wild; self.arity(c)]))
.collect();
if missing.is_empty() {
vec![Pat::Wild]
} else {
missing
}
}
fn variant(&self, sum_id: DefId, vi: u32) -> Option<&nova_hir::Variant> {
self.sums
.iter()
.find(|s| s.def_id == sum_id)
.and_then(|s| s.variants.get(vi as usize))
}
}
fn column_ctors(matrix: &[Vec<Pat>]) -> Vec<Ctor> {
let mut out: Vec<Ctor> = Vec::new();
for row in matrix {
if let Pat::Ctor(c, _) = &row[0] {
if !out.contains(c) {
out.push(c.clone());
}
}
}
out
}
fn default_matrix(matrix: &[Vec<Pat>]) -> Vec<Vec<Pat>> {
matrix
.iter()
.filter(|r| matches!(r[0], Pat::Wild))
.map(|r| r[1..].to_vec())
.collect()
}
fn reassemble(c: &Ctor, mut w: Witness, arity: usize) -> Witness {
let rest = w.split_off(arity.min(w.len()));
let mut row = vec![Pat::Ctor(c.clone(), w)];
row.extend(rest);
row
}
#[cfg(test)]
mod tests {
use super::*;
fn cx() -> MatchCx<'static> {
MatchCx::new(&[])
}
fn ctor(c: Ctor) -> Pat {
Pat::Ctor(c, Vec::new())
}
#[test]
fn bool_true_and_false_is_exhaustive() {
let matrix = vec![vec![ctor(Ctor::Bool(true))], vec![ctor(Ctor::Bool(false))]];
let w = cx().usefulness(&matrix, &[Pat::Wild], &[Ty::Bool]);
assert!(w.is_empty(), "expected exhaustive, got witnesses {w:?}");
}
#[test]
fn bool_missing_false_yields_false_witness() {
let matrix = vec![vec![ctor(Ctor::Bool(true))]];
let w = cx().usefulness(&matrix, &[Pat::Wild], &[Ty::Bool]);
assert_eq!(w.len(), 1);
assert!(matches!(w[0][0], Pat::Ctor(Ctor::Bool(false), _)));
}
#[test]
fn int_literals_need_a_wildcard() {
let matrix = vec![vec![ctor(Ctor::Int(0))], vec![ctor(Ctor::Int(1))]];
let w = cx().usefulness(&matrix, &[Pat::Wild], &[Ty::Int]);
assert_eq!(w.len(), 1);
assert!(matches!(w[0][0], Pat::Wild));
}
#[test]
fn int_with_wildcard_is_exhaustive() {
let matrix = vec![vec![ctor(Ctor::Int(0))], vec![Pat::Wild]];
let w = cx().usefulness(&matrix, &[Pat::Wild], &[Ty::Int]);
assert!(w.is_empty());
}
#[test]
fn arm_covered_by_earlier_wildcard_is_not_useful() {
let prior = vec![vec![Pat::Wild]];
let w = cx().usefulness(&prior, &[ctor(Ctor::Int(1))], &[Ty::Int]);
assert!(w.is_empty(), "arm should be unreachable");
}
#[test]
fn distinct_literal_is_useful() {
let prior = vec![vec![ctor(Ctor::Int(0))]];
let w = cx().usefulness(&prior, &[ctor(Ctor::Int(1))], &[Ty::Int]);
assert!(!w.is_empty(), "a fresh literal should be reachable");
}
}