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    /// A function carrying signature-level examples (#369) declares
68    /// at least one effect. v1 restricts examples to pure functions so
69    /// the "same inputs ⇒ same outputs" invariant (rule #5) holds
70    /// without modeling effect responses.
71    ExamplesOnEffectfulFn {
72        at_node: String,
73        fn_name: String,
74    },
75    /// A signature-level example case (#369) supplies the wrong number
76    /// of arguments for the function it documents.
77    ExampleArityMismatch {
78        at_node: String,
79        fn_name: String,
80        /// Zero-based index of the failing case in the `examples` block.
81        case_index: usize,
82        expected: usize,
83        got: usize,
84    },
85    /// A signature-level example case (#369 slice 2) ran successfully
86    /// but the function's actual output disagrees with the declared
87    /// `expected` value. This is the load-bearing check that makes the
88    /// `examples` block enforce behavior, not just types.
89    ExampleMismatch {
90        at_node: String,
91        fn_name: String,
92        case_index: usize,
93        /// Pretty-printed expected value (LHS of the `=>` in the example).
94        expected: String,
95        /// Pretty-printed actual value the function body produced.
96        got: String,
97    },
98}
99
100impl TypeError {
101    pub fn node(&self) -> &str {
102        match self {
103            TypeError::TypeMismatch { at_node, .. }
104            | TypeError::UnknownIdentifier { at_node, .. }
105            | TypeError::ArityMismatch { at_node, .. }
106            | TypeError::NonExhaustiveMatch { at_node, .. }
107            | TypeError::UnknownField { at_node, .. }
108            | TypeError::DuplicateField { at_node, .. }
109            | TypeError::UnknownVariant { at_node, .. }
110            | TypeError::EffectNotDeclared { at_node, .. }
111            | TypeError::InfiniteType { at_node, .. }
112            | TypeError::AmbiguousType { at_node, .. }
113            | TypeError::RecursiveTypeWithoutConstructor { at_node, .. }
114            | TypeError::RefinementViolation { at_node, .. }
115            | TypeError::ExamplesOnEffectfulFn { at_node, .. }
116            | TypeError::ExampleArityMismatch { at_node, .. }
117            | TypeError::ExampleMismatch { at_node, .. } => at_node,
118        }
119    }
120}
121
122impl std::fmt::Display for TypeError {
123    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
124        match self {
125            TypeError::TypeMismatch { at_node, expected, got, context } => {
126                write!(f, "type mismatch at {at_node}: expected {expected}, got {got}")?;
127                if !context.is_empty() { write!(f, " ({})", context.join(" / "))?; }
128                Ok(())
129            }
130            TypeError::UnknownIdentifier { at_node, name } => write!(f, "unknown identifier `{name}` at {at_node}"),
131            TypeError::ArityMismatch { at_node, expected, got } => write!(f, "arity mismatch at {at_node}: expected {expected}, got {got}"),
132            TypeError::NonExhaustiveMatch { at_node, missing } => write!(f, "non-exhaustive match at {at_node}: missing {missing:?}"),
133            TypeError::UnknownField { at_node, record_type, field } => write!(f, "unknown field `{field}` on {record_type} at {at_node}"),
134            TypeError::DuplicateField { at_node, field } => write!(f, "duplicate field `{field}` at {at_node}"),
135            TypeError::UnknownVariant { at_node, constructor } => write!(f, "unknown constructor `{constructor}` at {at_node}"),
136            TypeError::EffectNotDeclared { at_node, effect } => write!(f, "effect `{effect}` not declared at {at_node}"),
137            TypeError::InfiniteType { at_node } => write!(f, "infinite type (occurs check) at {at_node}"),
138            TypeError::AmbiguousType { at_node } => write!(f, "ambiguous type at {at_node}"),
139            TypeError::RecursiveTypeWithoutConstructor { at_node, name } => write!(f, "recursive type {name} has no constructor at {at_node}"),
140            TypeError::RefinementViolation { at_node, fn_name, param_index, binding, reason } =>
141                write!(f, "refinement violated at {at_node}: argument {} of `{fn_name}` (binding `{binding}`): {reason}",
142                    param_index + 1),
143            TypeError::ExamplesOnEffectfulFn { at_node, fn_name } =>
144                write!(f, "function `{fn_name}` at {at_node} carries `examples` but declares effects; \
145                          v1 restricts examples to pure functions"),
146            TypeError::ExampleArityMismatch { at_node, fn_name, case_index, expected, got } =>
147                write!(f, "example #{} of `{fn_name}` at {at_node}: expected {expected} argument(s), got {got}",
148                    case_index + 1),
149            TypeError::ExampleMismatch { at_node, fn_name, case_index, expected, got } =>
150                write!(f, "example #{} of `{fn_name}` at {at_node}: expected {expected}, got {got}",
151                    case_index + 1),
152        }
153    }
154}
155
156impl std::error::Error for TypeError {}
157
158/// `TypeError` enriched with an optional source `Position` (#306
159/// slice 1) plus a `rule_tag` + `rule_explanation` (#306 slice 2).
160/// `lex_types::check_program_with_positions` returns a
161/// `Vec<PositionedError>`; the bare `check_program` keeps the old
162/// `Vec<TypeError>` shape for backwards compatibility.
163///
164/// Serializes as a flat JSON object: the wrapped error's fields
165/// (`kind`, `at_node`, `expected`, …), the derived `rule_tag` +
166/// `rule_explanation`, and a `position` field when one was attached.
167/// Consumers can downcast via the `error` field or pattern-match on
168/// the `kind` tag in the JSON. The `rule_tag` is the stable
169/// kebab-case identifier LLM prompts should reference; the
170/// `rule_explanation` is a plain-language description of what the
171/// rule enforces, suitable to inline in a repair prompt.
172#[derive(Debug, Clone)]
173pub struct PositionedError {
174    pub error: TypeError,
175    pub position: Option<Position>,
176}
177
178impl Serialize for PositionedError {
179    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
180        use serde::ser::SerializeMap;
181        // Serialize the inner TypeError to a JSON map, then add the
182        // rule_tag/rule_explanation/position fields. This keeps the
183        // tagged-enum encoding (kind + fields) intact while flattening
184        // the extra computed fields on top.
185        let inner = serde_json::to_value(&self.error)
186            .map_err(serde::ser::Error::custom)?;
187        let obj = inner
188            .as_object()
189            .ok_or_else(|| serde::ser::Error::custom("TypeError did not serialize as an object"))?;
190        let extra = if self.position.is_some() { 3 } else { 2 };
191        let mut map = serializer.serialize_map(Some(obj.len() + extra))?;
192        for (k, v) in obj {
193            map.serialize_entry(k, v)?;
194        }
195        map.serialize_entry("rule_tag", self.error.rule_tag())?;
196        map.serialize_entry("rule_explanation", self.error.rule_explanation())?;
197        if let Some(p) = &self.position {
198            map.serialize_entry("position", p)?;
199        }
200        map.end()
201    }
202}
203
204impl<'de> Deserialize<'de> for PositionedError {
205    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
206        // Round-trip through a serde_json::Value so the embedded
207        // TypeError's tagged-enum encoding parses correctly even
208        // when the rule_tag/rule_explanation/position siblings are
209        // present.
210        let mut value = serde_json::Value::deserialize(deserializer)?;
211        let position = value
212            .as_object_mut()
213            .and_then(|o| o.remove("position"))
214            .and_then(|p| serde_json::from_value::<Position>(p).ok());
215        if let Some(o) = value.as_object_mut() {
216            o.remove("rule_tag");
217            o.remove("rule_explanation");
218        }
219        let error: TypeError =
220            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
221        Ok(PositionedError { error, position })
222    }
223}
224
225impl PositionedError {
226    pub fn new(error: TypeError, position: Option<Position>) -> Self {
227        Self { error, position }
228    }
229
230    pub fn without_position(error: TypeError) -> Self {
231        Self { error, position: None }
232    }
233
234    pub fn node(&self) -> &str {
235        self.error.node()
236    }
237}
238
239impl std::fmt::Display for PositionedError {
240    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
241        match &self.position {
242            Some(p) => write!(f, "[{}] {}", p.render(), self.error),
243            None => self.error.fmt(f),
244        }
245    }
246}
247
248impl std::error::Error for PositionedError {}
249
250impl From<TypeError> for PositionedError {
251    fn from(e: TypeError) -> Self {
252        Self::without_position(e)
253    }
254}