Skip to main content

nova_typeck/
lib.rs

1//! Type checker and inference for the Nova compiler.
2//!
3//! Implements the Phase 1 subset of `nova-spec/12-TYPESYSTEM.md`:
4//! Hindley-Milner style unification with explicit generics at function
5//! boundaries (no let-polymorphism), sum types with Maranget-based
6//! exhaustiveness/reachability checking, and desugaring of the AST into typed
7//! HIR (`nova-hir`).
8//!
9//! Literals are typed concretely (`Int` / `Float`), so the spec's numeric
10//! defaulting step is a no-op in this implementation.
11//!
12//! Trait resolution, records, closures and async are later Phase 1/2 steps;
13//! encountering them reports an `E0900` "not supported yet" diagnostic
14//! rather than panicking.
15
16mod check;
17mod infer;
18mod usefulness;
19
20pub use check::{check, check_with};
21
22use nova_hir::Ty;
23use nova_resolver::Definitions;
24
25/// Result of type checking one file.
26#[derive(Debug)]
27pub struct CheckResult {
28    /// The typed module. Partially populated when errors were reported;
29    /// only meaningful for codegen when `diagnostics` has no errors.
30    pub module: nova_hir::Module,
31    pub diagnostics: Vec<nova_diagnostics::Diagnostic>,
32    /// What the probe found: empty unless [`check_with`] was given one.
33    pub probe: ProbeResult,
34    /// What the index recorded: `None` unless [`CheckOptions::index`].
35    pub index: Option<nova_resolver::Index>,
36}
37
38/// Where the language server's probe looks: a byte offset in one file (spec
39/// `docs/superpowers/specs/2026-10-08-phase-3-2-lsp-core-design.md` §4.1).
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct ProbePoint {
42    pub file: nova_diagnostics::FileId,
43    pub offset: u32,
44}
45
46/// What [`check_with`] does beyond checking.
47#[derive(Debug, Clone, Copy, Default)]
48pub struct CheckOptions<'a> {
49    pub probe: Option<ProbePoint>,
50    /// Record the language server's index (spec
51    /// `docs/superpowers/specs/2026-10-10-phase-3-4a-navigation-design.md`
52    /// §3). Recording is write-only: the checker never reads it.
53    pub index: bool,
54    /// The program's sources, for the fixes placed by lines (spec
55    /// `docs/superpowers/specs/2026-10-10-phase-3-4b-fixes-and-colour-design.md`
56    /// §4.7). Without them, those fixes are not offered.
57    pub sources: Option<&'a nova_diagnostics::FileDb>,
58}
59
60/// What the probe found (spec §4.1, §4.2). Each part is empty when the
61/// probe's place holds nothing of its kind.
62#[derive(Debug, Clone, Default, PartialEq)]
63pub struct ProbeResult {
64    /// At a member access whose receiver ends before the offset and whose
65    /// name ends at or after it: the receiver's type.
66    pub receiver: Option<Ty>,
67    /// The receiver's fields and methods.
68    pub members: Vec<Member>,
69    /// The locals in scope at the offset, innermost first, each name once.
70    pub locals: Vec<(String, Ty)>,
71}
72
73/// A field or method completion can offer after `.`.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Member {
76    pub name: String,
77    pub kind: MemberKind,
78    /// Where it is declared: a field's type, or a method's name, from which
79    /// the server reads the signature. `None` for a built-in method.
80    pub decl: Option<nova_diagnostics::Span>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum MemberKind {
85    Field,
86    Method,
87}
88
89/// Render a type for use in diagnostics, resolving sum-type names.
90pub fn display_ty(ty: &Ty, defs: &Definitions) -> String {
91    match ty {
92        Ty::Int => "Int".to_string(),
93        Ty::Float => "Float".to_string(),
94        Ty::Bool => "Bool".to_string(),
95        Ty::Char => "Char".to_string(),
96        Ty::String => "String".to_string(),
97        Ty::Bytes => "Bytes".to_string(),
98        Ty::Unit => "()".to_string(),
99        Ty::Fn { params, ret } => {
100            let params = params
101                .iter()
102                .map(|p| display_ty(p, defs))
103                .collect::<Vec<_>>()
104                .join(", ");
105            format!("fn({params}) -> {}", display_ty(ret, defs))
106        }
107        Ty::Sum { def_id, args } | Ty::Record { def_id, args } => {
108            let name = &defs.def(*def_id).name;
109            if args.is_empty() {
110                name.clone()
111            } else {
112                let args = args
113                    .iter()
114                    .map(|a| display_ty(a, defs))
115                    .collect::<Vec<_>>()
116                    .join(", ");
117                format!("{name}<{args}>")
118            }
119        }
120        Ty::Array(elem) => format!("[{}]", display_ty(elem, defs)),
121        Ty::Future(out) => format!("Future<{}>", display_ty(out, defs)),
122        Ty::Param(i) => format!("T{i}"),
123        Ty::Assoc { on, assoc } => {
124            format!("{}::{}", display_ty(on, defs), defs.def(*assoc).name)
125        }
126        Ty::Var(v) => format!("?{v}"),
127        Ty::Never => "!".to_string(),
128        Ty::Error => "{error}".to_string(),
129    }
130}
131
132/// [`display_ty`] for hover (spec 3.4a §3.4): type parameter `i` prints as
133/// `names[i]`, and an unsolved variable or an error as `_`.
134pub fn display_ty_named(ty: &Ty, defs: &Definitions, names: &[String]) -> String {
135    let show = |t: &Ty| display_ty_named(t, defs, names);
136    let list = |ts: &[Ty]| ts.iter().map(show).collect::<Vec<_>>().join(", ");
137    match ty {
138        Ty::Fn { params, ret } => format!("fn({}) -> {}", list(params.as_slice()), show(ret)),
139        Ty::Sum { def_id, args } | Ty::Record { def_id, args } => {
140            let name = &defs.def(*def_id).name;
141            if args.is_empty() {
142                name.clone()
143            } else {
144                format!("{name}<{}>", list(args.as_slice()))
145            }
146        }
147        Ty::Array(elem) => format!("[{}]", show(elem)),
148        Ty::Future(out) => format!("Future<{}>", show(out)),
149        Ty::Param(i) => names
150            .get(*i as usize)
151            .filter(|n| !n.is_empty())
152            .cloned()
153            .unwrap_or_else(|| format!("T{i}")),
154        Ty::Assoc { on, assoc } => format!("{}::{}", show(on), defs.def(*assoc).name),
155        Ty::Var(_) | Ty::Error => "_".to_string(),
156        other => display_ty(other, defs),
157    }
158}
159
160/// A builtin's signature for hover (spec 3.4a §5.1), its type parameters
161/// as `T`, `U`, …: `fn print(String) -> ()`.
162pub fn builtin_text(builtin: nova_resolver::Builtin, defs: &Definitions) -> String {
163    let (params, ret) = check::builtin_signature(builtin);
164    let names: Vec<String> = ["T", "U", "V", "W"].iter().map(|s| s.to_string()).collect();
165    let params = params
166        .iter()
167        .map(|p| display_ty_named(p, defs, &names))
168        .collect::<Vec<_>>()
169        .join(", ");
170    format!(
171        "fn {}({params}) -> {}",
172        builtin.name(),
173        display_ty_named(&ret, defs, &names)
174    )
175}