Skip to main content

lex_types/
error.rs

1//! Structured type errors per spec §6.7.
2
3use crate::position::Position;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(tag = "kind", rename_all = "snake_case")]
8pub enum TypeError {
9    TypeMismatch {
10        at_node: String,
11        expected: String,
12        got: String,
13        context: Vec<String>,
14    },
15    UnknownIdentifier {
16        at_node: String,
17        name: String,
18    },
19    ArityMismatch {
20        at_node: String,
21        expected: usize,
22        got: usize,
23    },
24    NonExhaustiveMatch {
25        at_node: String,
26        missing: Vec<String>,
27    },
28    UnknownField {
29        at_node: String,
30        record_type: String,
31        field: String,
32    },
33    DuplicateField {
34        at_node: String,
35        field: String,
36    },
37    UnknownVariant {
38        at_node: String,
39        constructor: String,
40    },
41    EffectNotDeclared {
42        at_node: String,
43        effect: String,
44    },
45    InfiniteType {
46        at_node: String,
47    },
48    AmbiguousType {
49        at_node: String,
50    },
51    RecursiveTypeWithoutConstructor {
52        at_node: String,
53        name: String,
54    },
55    /// Refinement-type predicate provably violated at a call site
56    /// (#209 slice 2). The type checker statically discharged the
57    /// refinement and found the literal argument doesn't satisfy the
58    /// predicate. Slice 3 will add residual runtime checks for
59    /// arguments that can't be discharged statically.
60    RefinementViolation {
61        at_node: String,
62        fn_name: String,
63        param_index: usize,
64        binding: String,
65        reason: String,
66    },
67}
68
69impl TypeError {
70    pub fn node(&self) -> &str {
71        match self {
72            TypeError::TypeMismatch { at_node, .. }
73            | TypeError::UnknownIdentifier { at_node, .. }
74            | TypeError::ArityMismatch { at_node, .. }
75            | TypeError::NonExhaustiveMatch { at_node, .. }
76            | TypeError::UnknownField { at_node, .. }
77            | TypeError::DuplicateField { at_node, .. }
78            | TypeError::UnknownVariant { at_node, .. }
79            | TypeError::EffectNotDeclared { at_node, .. }
80            | TypeError::InfiniteType { at_node, .. }
81            | TypeError::AmbiguousType { at_node, .. }
82            | TypeError::RecursiveTypeWithoutConstructor { at_node, .. }
83            | TypeError::RefinementViolation { at_node, .. } => at_node,
84        }
85    }
86}
87
88impl std::fmt::Display for TypeError {
89    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
90        match self {
91            TypeError::TypeMismatch { at_node, expected, got, context } => {
92                write!(f, "type mismatch at {at_node}: expected {expected}, got {got}")?;
93                if !context.is_empty() { write!(f, " ({})", context.join(" / "))?; }
94                Ok(())
95            }
96            TypeError::UnknownIdentifier { at_node, name } => write!(f, "unknown identifier `{name}` at {at_node}"),
97            TypeError::ArityMismatch { at_node, expected, got } => write!(f, "arity mismatch at {at_node}: expected {expected}, got {got}"),
98            TypeError::NonExhaustiveMatch { at_node, missing } => write!(f, "non-exhaustive match at {at_node}: missing {missing:?}"),
99            TypeError::UnknownField { at_node, record_type, field } => write!(f, "unknown field `{field}` on {record_type} at {at_node}"),
100            TypeError::DuplicateField { at_node, field } => write!(f, "duplicate field `{field}` at {at_node}"),
101            TypeError::UnknownVariant { at_node, constructor } => write!(f, "unknown constructor `{constructor}` at {at_node}"),
102            TypeError::EffectNotDeclared { at_node, effect } => write!(f, "effect `{effect}` not declared at {at_node}"),
103            TypeError::InfiniteType { at_node } => write!(f, "infinite type (occurs check) at {at_node}"),
104            TypeError::AmbiguousType { at_node } => write!(f, "ambiguous type at {at_node}"),
105            TypeError::RecursiveTypeWithoutConstructor { at_node, name } => write!(f, "recursive type {name} has no constructor at {at_node}"),
106            TypeError::RefinementViolation { at_node, fn_name, param_index, binding, reason } =>
107                write!(f, "refinement violated at {at_node}: argument {} of `{fn_name}` (binding `{binding}`): {reason}",
108                    param_index + 1),
109        }
110    }
111}
112
113impl std::error::Error for TypeError {}
114
115/// `TypeError` enriched with an optional source `Position` (#306
116/// slice 1) plus a `rule_tag` + `rule_explanation` (#306 slice 2).
117/// `lex_types::check_program_with_positions` returns a
118/// `Vec<PositionedError>`; the bare `check_program` keeps the old
119/// `Vec<TypeError>` shape for backwards compatibility.
120///
121/// Serializes as a flat JSON object: the wrapped error's fields
122/// (`kind`, `at_node`, `expected`, …), the derived `rule_tag` +
123/// `rule_explanation`, and a `position` field when one was attached.
124/// Consumers can downcast via the `error` field or pattern-match on
125/// the `kind` tag in the JSON. The `rule_tag` is the stable
126/// kebab-case identifier LLM prompts should reference; the
127/// `rule_explanation` is a plain-language description of what the
128/// rule enforces, suitable to inline in a repair prompt.
129#[derive(Debug, Clone)]
130pub struct PositionedError {
131    pub error: TypeError,
132    pub position: Option<Position>,
133}
134
135impl Serialize for PositionedError {
136    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
137        use serde::ser::SerializeMap;
138        // Serialize the inner TypeError to a JSON map, then add the
139        // rule_tag/rule_explanation/position fields. This keeps the
140        // tagged-enum encoding (kind + fields) intact while flattening
141        // the extra computed fields on top.
142        let inner = serde_json::to_value(&self.error)
143            .map_err(serde::ser::Error::custom)?;
144        let obj = inner
145            .as_object()
146            .ok_or_else(|| serde::ser::Error::custom("TypeError did not serialize as an object"))?;
147        let extra = if self.position.is_some() { 3 } else { 2 };
148        let mut map = serializer.serialize_map(Some(obj.len() + extra))?;
149        for (k, v) in obj {
150            map.serialize_entry(k, v)?;
151        }
152        map.serialize_entry("rule_tag", self.error.rule_tag())?;
153        map.serialize_entry("rule_explanation", self.error.rule_explanation())?;
154        if let Some(p) = &self.position {
155            map.serialize_entry("position", p)?;
156        }
157        map.end()
158    }
159}
160
161impl<'de> Deserialize<'de> for PositionedError {
162    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163        // Round-trip through a serde_json::Value so the embedded
164        // TypeError's tagged-enum encoding parses correctly even
165        // when the rule_tag/rule_explanation/position siblings are
166        // present.
167        let mut value = serde_json::Value::deserialize(deserializer)?;
168        let position = value
169            .as_object_mut()
170            .and_then(|o| o.remove("position"))
171            .and_then(|p| serde_json::from_value::<Position>(p).ok());
172        if let Some(o) = value.as_object_mut() {
173            o.remove("rule_tag");
174            o.remove("rule_explanation");
175        }
176        let error: TypeError =
177            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
178        Ok(PositionedError { error, position })
179    }
180}
181
182impl PositionedError {
183    pub fn new(error: TypeError, position: Option<Position>) -> Self {
184        Self { error, position }
185    }
186
187    pub fn without_position(error: TypeError) -> Self {
188        Self { error, position: None }
189    }
190
191    pub fn node(&self) -> &str {
192        self.error.node()
193    }
194}
195
196impl std::fmt::Display for PositionedError {
197    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
198        match &self.position {
199            Some(p) => write!(f, "[{}] {}", p.render(), self.error),
200            None => self.error.fmt(f),
201        }
202    }
203}
204
205impl std::error::Error for PositionedError {}
206
207impl From<TypeError> for PositionedError {
208    fn from(e: TypeError) -> Self {
209        Self::without_position(e)
210    }
211}