Skip to main content

harn_parser/typechecker/
format.rs

1//! Display helpers for type expressions and shape mismatches.
2//!
3//! `format_type` is the canonical pretty-printer for `TypeExpr` (also used
4//! by `harn-lsp` and `harn-fmt` via re-export). `shape_mismatch_detail`
5//! produces a one-line "missing field …" / "field 'x' has type …" diff that
6//! enriches type-error messages.
7
8use crate::ast::*;
9
10/// Pretty-print a type expression for display in error messages.
11pub fn format_type(ty: &TypeExpr) -> String {
12    match ty {
13        TypeExpr::Named(n) => n.clone(),
14        TypeExpr::Union(types) => {
15            if let Some(inner) = optional_sugar_inner(types) {
16                return format!("{}?", format_type(inner));
17            }
18            types
19                .iter()
20                .map(format_type)
21                .collect::<Vec<_>>()
22                .join(" | ")
23        }
24        TypeExpr::Intersection(types) => types
25            .iter()
26            .map(|m| match m {
27                // `T | nil` arms render as the sugared `T?`, which binds
28                // tighter than `&` and reads back unambiguously.
29                TypeExpr::Union(members) if optional_sugar_inner(members).is_some() => {
30                    format_type(m)
31                }
32                // Other nested unions still get parenthesised for readability.
33                TypeExpr::Union(_) => format!("({})", format_type(m)),
34                _ => format_type(m),
35            })
36            .collect::<Vec<_>>()
37            .join(" & "),
38        TypeExpr::Shape(fields) => {
39            let inner: Vec<String> = fields
40                .iter()
41                .map(|f| {
42                    let opt = if f.optional { "?" } else { "" };
43                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
44                })
45                .collect();
46            format!("{{{}}}", inner.join(", "))
47        }
48        TypeExpr::OpenShape { fields, rests } => {
49            let mut parts: Vec<String> = fields
50                .iter()
51                .map(|f| {
52                    let opt = if f.optional { "?" } else { "" };
53                    format!("{}{opt}: {}", f.name, format_type(&f.type_expr))
54                })
55                .collect();
56            for rest in rests {
57                parts.push(format!("...{}", format_type(rest)));
58            }
59            format!("{{{}}}", parts.join(", "))
60        }
61        TypeExpr::List(inner) => format!("list<{}>", format_type(inner)),
62        TypeExpr::Tuple(elements) => {
63            let elements = elements
64                .iter()
65                .map(format_type)
66                .collect::<Vec<_>>()
67                .join(", ");
68            format!("tuple<{elements}>")
69        }
70        TypeExpr::Iter(inner) => format!("iter<{}>", format_type(inner)),
71        TypeExpr::Generator(inner) => format!("Generator<{}>", format_type(inner)),
72        TypeExpr::Stream(inner) => format!("Stream<{}>", format_type(inner)),
73        TypeExpr::DictType(k, v) => format!("dict<{}, {}>", format_type(k), format_type(v)),
74        TypeExpr::Applied { name, args } => {
75            let args_str = args.iter().map(format_type).collect::<Vec<_>>().join(", ");
76            format!("{name}<{args_str}>")
77        }
78        TypeExpr::FnType {
79            params,
80            return_type,
81        } => {
82            let params_str = params
83                .iter()
84                .map(format_type)
85                .collect::<Vec<_>>()
86                .join(", ");
87            format!("fn({}) -> {}", params_str, format_type(return_type))
88        }
89        TypeExpr::Never => "never".to_string(),
90        TypeExpr::LitString(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
91        TypeExpr::LitInt(v) => v.to_string(),
92        TypeExpr::Owned(inner) => format!("owned<{}>", format_type(inner)),
93    }
94}
95
96/// Produce a detail string describing why a Shape type is incompatible with
97/// another Shape type — e.g. "missing field 'age' (int)" or "field 'name'
98/// has type int, expected string". Returns `None` if both types are not shapes.
99pub fn shape_mismatch_detail(expected: &TypeExpr, actual: &TypeExpr) -> Option<String> {
100    if let (TypeExpr::Shape(ef), TypeExpr::Shape(af)) = (expected, actual) {
101        let mut details = Vec::new();
102        for field in ef {
103            if field.optional {
104                continue;
105            }
106            match af.iter().find(|f| f.name == field.name) {
107                None => details.push(format!(
108                    "missing field '{}' ({})",
109                    field.name,
110                    format_type(&field.type_expr)
111                )),
112                Some(actual_field) => {
113                    let e_str = format_type(&field.type_expr);
114                    let a_str = format_type(&actual_field.type_expr);
115                    if e_str != a_str {
116                        details.push(format!(
117                            "field '{}' has type {}, expected {}",
118                            field.name, a_str, e_str
119                        ));
120                    }
121                }
122            }
123        }
124        if details.is_empty() {
125            None
126        } else {
127            Some(details.join("; "))
128        }
129    } else {
130        None
131    }
132}
133
134/// If `types` is exactly two members and one is `nil`, return the
135/// non-`nil` member when it can be safely rendered as `T?`. Mirrors the
136/// formatter's rule in `harn-fmt::helpers::optional_sugar_inner`: only
137/// types that appear at primary precedence (or below) can be sugared,
138/// because postfix `?` parses tighter than `&` / `|` / `fn(...) -> ...`
139/// return positions.
140fn optional_sugar_inner(types: &[TypeExpr]) -> Option<&TypeExpr> {
141    if types.len() != 2 {
142        return None;
143    }
144    let nil_idx = types
145        .iter()
146        .position(|t| matches!(t, TypeExpr::Named(n) if n == "nil"))?;
147    let inner = &types[1 - nil_idx];
148    if matches!(
149        inner,
150        TypeExpr::Union(_) | TypeExpr::Intersection(_) | TypeExpr::FnType { .. }
151    ) {
152        return None;
153    }
154    if matches!(inner, TypeExpr::Named(n) if n == "nil") {
155        return None;
156    }
157    Some(inner)
158}
159
160/// Returns true when the type is obvious from the RHS expression
161/// (e.g. `let x = 42` is obviously int — no hint needed).
162pub(super) fn is_obvious_type(value: &SNode, _ty: &TypeExpr) -> bool {
163    matches!(
164        &value.node,
165        Node::IntLiteral(_)
166            | Node::FloatLiteral(_)
167            | Node::StringLiteral(_)
168            | Node::BoolLiteral(_)
169            | Node::NilLiteral
170            | Node::ListLiteral(_)
171            | Node::DictLiteral(_)
172            | Node::InterpolatedString(_)
173    )
174}