nova-typeck 0.3.2

The Nova language's type checker (internal to the nova toolchain)
Documentation
//! Type checker and inference for the Nova compiler.
//!
//! Implements the Phase 1 subset of `nova-spec/12-TYPESYSTEM.md`:
//! Hindley-Milner style unification with explicit generics at function
//! boundaries (no let-polymorphism), sum types with Maranget-based
//! exhaustiveness/reachability checking, and desugaring of the AST into typed
//! HIR (`nova-hir`).
//!
//! Literals are typed concretely (`Int` / `Float`), so the spec's numeric
//! defaulting step is a no-op in this implementation.
//!
//! Trait resolution, records, closures and async are later Phase 1/2 steps;
//! encountering them reports an `E0900` "not supported yet" diagnostic
//! rather than panicking.

mod check;
mod infer;
mod usefulness;

pub use check::{check, check_with};

use nova_hir::Ty;
use nova_resolver::Definitions;

/// Result of type checking one file.
#[derive(Debug)]
pub struct CheckResult {
    /// The typed module. Partially populated when errors were reported;
    /// only meaningful for codegen when `diagnostics` has no errors.
    pub module: nova_hir::Module,
    pub diagnostics: Vec<nova_diagnostics::Diagnostic>,
    /// What the probe found: empty unless [`check_with`] was given one.
    pub probe: ProbeResult,
    /// What the index recorded: `None` unless [`CheckOptions::index`].
    pub index: Option<nova_resolver::Index>,
}

/// Where the language server's probe looks: a byte offset in one file (spec
/// `docs/superpowers/specs/2026-10-08-phase-3-2-lsp-core-design.md` §4.1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProbePoint {
    pub file: nova_diagnostics::FileId,
    pub offset: u32,
}

/// What [`check_with`] does beyond checking.
#[derive(Debug, Clone, Copy, Default)]
pub struct CheckOptions<'a> {
    pub probe: Option<ProbePoint>,
    /// Record the language server's index (spec
    /// `docs/superpowers/specs/2026-10-10-phase-3-4a-navigation-design.md`
    /// §3). Recording is write-only: the checker never reads it.
    pub index: bool,
    /// The program's sources, for the fixes placed by lines (spec
    /// `docs/superpowers/specs/2026-10-10-phase-3-4b-fixes-and-colour-design.md`
    /// §4.7). Without them, those fixes are not offered.
    pub sources: Option<&'a nova_diagnostics::FileDb>,
}

/// What the probe found (spec §4.1, §4.2). Each part is empty when the
/// probe's place holds nothing of its kind.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ProbeResult {
    /// At a member access whose receiver ends before the offset and whose
    /// name ends at or after it: the receiver's type.
    pub receiver: Option<Ty>,
    /// The receiver's fields and methods.
    pub members: Vec<Member>,
    /// The locals in scope at the offset, innermost first, each name once.
    pub locals: Vec<(String, Ty)>,
}

/// A field or method completion can offer after `.`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Member {
    pub name: String,
    pub kind: MemberKind,
    /// Where it is declared: a field's type, or a method's name, from which
    /// the server reads the signature. `None` for a built-in method.
    pub decl: Option<nova_diagnostics::Span>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberKind {
    Field,
    Method,
}

/// Render a type for use in diagnostics, resolving sum-type names.
pub fn display_ty(ty: &Ty, defs: &Definitions) -> String {
    match ty {
        Ty::Int => "Int".to_string(),
        Ty::Float => "Float".to_string(),
        Ty::Bool => "Bool".to_string(),
        Ty::Char => "Char".to_string(),
        Ty::String => "String".to_string(),
        Ty::Bytes => "Bytes".to_string(),
        Ty::Unit => "()".to_string(),
        Ty::Fn { params, ret } => {
            let params = params
                .iter()
                .map(|p| display_ty(p, defs))
                .collect::<Vec<_>>()
                .join(", ");
            format!("fn({params}) -> {}", display_ty(ret, defs))
        }
        Ty::Sum { def_id, args } | Ty::Record { def_id, args } => {
            let name = &defs.def(*def_id).name;
            if args.is_empty() {
                name.clone()
            } else {
                let args = args
                    .iter()
                    .map(|a| display_ty(a, defs))
                    .collect::<Vec<_>>()
                    .join(", ");
                format!("{name}<{args}>")
            }
        }
        Ty::Array(elem) => format!("[{}]", display_ty(elem, defs)),
        Ty::Future(out) => format!("Future<{}>", display_ty(out, defs)),
        Ty::Param(i) => format!("T{i}"),
        Ty::Assoc { on, assoc } => {
            format!("{}::{}", display_ty(on, defs), defs.def(*assoc).name)
        }
        Ty::Var(v) => format!("?{v}"),
        Ty::Never => "!".to_string(),
        Ty::Error => "{error}".to_string(),
    }
}

/// [`display_ty`] for hover (spec 3.4a §3.4): type parameter `i` prints as
/// `names[i]`, and an unsolved variable or an error as `_`.
pub fn display_ty_named(ty: &Ty, defs: &Definitions, names: &[String]) -> String {
    let show = |t: &Ty| display_ty_named(t, defs, names);
    let list = |ts: &[Ty]| ts.iter().map(show).collect::<Vec<_>>().join(", ");
    match ty {
        Ty::Fn { params, ret } => format!("fn({}) -> {}", list(params.as_slice()), show(ret)),
        Ty::Sum { def_id, args } | Ty::Record { def_id, args } => {
            let name = &defs.def(*def_id).name;
            if args.is_empty() {
                name.clone()
            } else {
                format!("{name}<{}>", list(args.as_slice()))
            }
        }
        Ty::Array(elem) => format!("[{}]", show(elem)),
        Ty::Future(out) => format!("Future<{}>", show(out)),
        Ty::Param(i) => names
            .get(*i as usize)
            .filter(|n| !n.is_empty())
            .cloned()
            .unwrap_or_else(|| format!("T{i}")),
        Ty::Assoc { on, assoc } => format!("{}::{}", show(on), defs.def(*assoc).name),
        Ty::Var(_) | Ty::Error => "_".to_string(),
        other => display_ty(other, defs),
    }
}

/// A builtin's signature for hover (spec 3.4a §5.1), its type parameters
/// as `T`, `U`, …: `fn print(String) -> ()`.
pub fn builtin_text(builtin: nova_resolver::Builtin, defs: &Definitions) -> String {
    let (params, ret) = check::builtin_signature(builtin);
    let names: Vec<String> = ["T", "U", "V", "W"].iter().map(|s| s.to_string()).collect();
    let params = params
        .iter()
        .map(|p| display_ty_named(p, defs, &names))
        .collect::<Vec<_>>()
        .join(", ");
    format!(
        "fn {}({params}) -> {}",
        builtin.name(),
        display_ty_named(&ret, defs, &names)
    )
}