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    /// Two function types failed to unify specifically because their
16    /// effect rows differ (#565). Effect rows unify by *equality*, not
17    /// subtyping — a concrete row must match exactly, not merely be a
18    /// superset or subset. This most often surfaces on record-field
19    /// closures (e.g. `Skill.handle`, `Tool.execute`) whose declared
20    /// row is fixed by the record type. Split out from `TypeMismatch`
21    /// so the `rule_tag` names the invariance and the suggested fix
22    /// steers toward narrowing the body rather than broadening the row.
23    EffectRowMismatch {
24        at_node: String,
25        /// Pretty-printed expected effect row, e.g. `[io, net]`.
26        expected: String,
27        /// Pretty-printed actual effect row found.
28        got: String,
29        context: Vec<String>,
30    },
31    UnknownIdentifier {
32        at_node: String,
33        name: String,
34    },
35    ArityMismatch {
36        at_node: String,
37        expected: usize,
38        got: usize,
39    },
40    NonExhaustiveMatch {
41        at_node: String,
42        missing: Vec<String>,
43    },
44    UnknownField {
45        at_node: String,
46        record_type: String,
47        field: String,
48    },
49    DuplicateField {
50        at_node: String,
51        field: String,
52    },
53    UnknownVariant {
54        at_node: String,
55        constructor: String,
56    },
57    EffectNotDeclared {
58        at_node: String,
59        effect: String,
60    },
61    InfiniteType {
62        at_node: String,
63    },
64    AmbiguousType {
65        at_node: String,
66    },
67    RecursiveTypeWithoutConstructor {
68        at_node: String,
69        name: String,
70    },
71    /// Refinement-type predicate provably violated at a call site
72    /// (#209 slice 2). The type checker statically discharged the
73    /// refinement and found the literal argument doesn't satisfy the
74    /// predicate. Slice 3 will add residual runtime checks for
75    /// arguments that can't be discharged statically.
76    RefinementViolation {
77        at_node: String,
78        fn_name: String,
79        param_index: usize,
80        binding: String,
81        reason: String,
82    },
83    /// A function carrying signature-level examples (#369) declares
84    /// at least one effect. v1 restricts examples to pure functions so
85    /// the "same inputs ⇒ same outputs" invariant (rule #5) holds
86    /// without modeling effect responses.
87    ExamplesOnEffectfulFn {
88        at_node: String,
89        fn_name: String,
90    },
91    /// A signature-level example case (#369) supplies the wrong number
92    /// of arguments for the function it documents.
93    ExampleArityMismatch {
94        at_node: String,
95        fn_name: String,
96        /// Zero-based index of the failing case in the `examples` block.
97        case_index: usize,
98        expected: usize,
99        got: usize,
100    },
101    /// A signature-level example case (#369 slice 2) ran successfully
102    /// but the function's actual output disagrees with the declared
103    /// `expected` value. This is the load-bearing check that makes the
104    /// `examples` block enforce behavior, not just types.
105    ExampleMismatch {
106        at_node: String,
107        fn_name: String,
108        case_index: usize,
109        /// Pretty-printed expected value (LHS of the `=>` in the example).
110        expected: String,
111        /// Pretty-printed actual value the function body produced.
112        got: String,
113    },
114}
115
116impl TypeError {
117    pub fn node(&self) -> &str {
118        match self {
119            TypeError::TypeMismatch { at_node, .. }
120            | TypeError::EffectRowMismatch { at_node, .. }
121            | TypeError::UnknownIdentifier { at_node, .. }
122            | TypeError::ArityMismatch { at_node, .. }
123            | TypeError::NonExhaustiveMatch { at_node, .. }
124            | TypeError::UnknownField { at_node, .. }
125            | TypeError::DuplicateField { at_node, .. }
126            | TypeError::UnknownVariant { at_node, .. }
127            | TypeError::EffectNotDeclared { at_node, .. }
128            | TypeError::InfiniteType { at_node, .. }
129            | TypeError::AmbiguousType { at_node, .. }
130            | TypeError::RecursiveTypeWithoutConstructor { at_node, .. }
131            | TypeError::RefinementViolation { at_node, .. }
132            | TypeError::ExamplesOnEffectfulFn { at_node, .. }
133            | TypeError::ExampleArityMismatch { at_node, .. }
134            | TypeError::ExampleMismatch { at_node, .. } => at_node,
135        }
136    }
137}
138
139impl std::fmt::Display for TypeError {
140    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
141        match self {
142            TypeError::TypeMismatch { at_node, expected, got, context } => {
143                write!(f, "type mismatch at {at_node}: expected {expected}, got {got}")?;
144                if !context.is_empty() { write!(f, " ({})", context.join(" / "))?; }
145                Ok(())
146            }
147            TypeError::EffectRowMismatch { at_node, expected, got, context } => {
148                write!(f, "effect-row mismatch at {at_node}: declared row is invariant — expected {expected}, got {got}; narrow the body, don't broaden the row")?;
149                if !context.is_empty() { write!(f, " ({})", context.join(" / "))?; }
150                Ok(())
151            }
152            TypeError::UnknownIdentifier { at_node, name } => write!(f, "unknown identifier `{name}` at {at_node}"),
153            TypeError::ArityMismatch { at_node, expected, got } => write!(f, "arity mismatch at {at_node}: expected {expected}, got {got}"),
154            TypeError::NonExhaustiveMatch { at_node, missing } => write!(f, "non-exhaustive match at {at_node}: missing {missing:?}"),
155            TypeError::UnknownField { at_node, record_type, field } => write!(f, "unknown field `{field}` on {record_type} at {at_node}"),
156            TypeError::DuplicateField { at_node, field } => write!(f, "duplicate field `{field}` at {at_node}"),
157            TypeError::UnknownVariant { at_node, constructor } => write!(f, "unknown constructor `{constructor}` at {at_node}"),
158            TypeError::EffectNotDeclared { at_node, effect } => write!(f, "effect `{effect}` not declared at {at_node}"),
159            TypeError::InfiniteType { at_node } => write!(f, "infinite type (occurs check) at {at_node}"),
160            TypeError::AmbiguousType { at_node } => write!(f, "ambiguous type at {at_node}"),
161            TypeError::RecursiveTypeWithoutConstructor { at_node, name } => write!(f, "recursive type {name} has no constructor at {at_node}"),
162            TypeError::RefinementViolation { at_node, fn_name, param_index, binding, reason } =>
163                write!(f, "refinement violated at {at_node}: argument {} of `{fn_name}` (binding `{binding}`): {reason}",
164                    param_index + 1),
165            TypeError::ExamplesOnEffectfulFn { at_node, fn_name } =>
166                write!(f, "function `{fn_name}` at {at_node} carries `examples` but declares effects; \
167                          v1 restricts examples to pure functions"),
168            TypeError::ExampleArityMismatch { at_node, fn_name, case_index, expected, got } =>
169                write!(f, "example #{} of `{fn_name}` at {at_node}: expected {expected} argument(s), got {got}",
170                    case_index + 1),
171            TypeError::ExampleMismatch { at_node, fn_name, case_index, expected, got } =>
172                write!(f, "example #{} of `{fn_name}` at {at_node}: expected {expected}, got {got}",
173                    case_index + 1),
174        }
175    }
176}
177
178impl std::error::Error for TypeError {}
179
180/// `TypeError` enriched with an optional source `Position` (#306
181/// slice 1) plus a `rule_tag` + `rule_explanation` (#306 slice 2).
182/// `lex_types::check_program_with_positions` returns a
183/// `Vec<PositionedError>`; the bare `check_program` keeps the old
184/// `Vec<TypeError>` shape for backwards compatibility.
185///
186/// Serializes as a flat JSON object: the wrapped error's fields
187/// (`kind`, `at_node`, `expected`, …), the derived `rule_tag` +
188/// `rule_explanation`, and a `position` field when one was attached.
189/// Consumers can downcast via the `error` field or pattern-match on
190/// the `kind` tag in the JSON. The `rule_tag` is the stable
191/// kebab-case identifier LLM prompts should reference; the
192/// `rule_explanation` is a plain-language description of what the
193/// rule enforces, suitable to inline in a repair prompt.
194#[derive(Debug, Clone)]
195pub struct PositionedError {
196    pub error: TypeError,
197    pub position: Option<Position>,
198}
199
200impl Serialize for PositionedError {
201    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
202        use serde::ser::SerializeMap;
203        // Serialize the inner TypeError to a JSON map, then add the
204        // rule_tag/rule_explanation/position fields. This keeps the
205        // tagged-enum encoding (kind + fields) intact while flattening
206        // the extra computed fields on top.
207        let inner = serde_json::to_value(&self.error)
208            .map_err(serde::ser::Error::custom)?;
209        let obj = inner
210            .as_object()
211            .ok_or_else(|| serde::ser::Error::custom("TypeError did not serialize as an object"))?;
212        let extra = if self.position.is_some() { 3 } else { 2 };
213        let mut map = serializer.serialize_map(Some(obj.len() + extra))?;
214        for (k, v) in obj {
215            map.serialize_entry(k, v)?;
216        }
217        map.serialize_entry("rule_tag", self.error.rule_tag())?;
218        map.serialize_entry("rule_explanation", self.error.rule_explanation())?;
219        if let Some(p) = &self.position {
220            map.serialize_entry("position", p)?;
221        }
222        map.end()
223    }
224}
225
226impl<'de> Deserialize<'de> for PositionedError {
227    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
228        // Round-trip through a serde_json::Value so the embedded
229        // TypeError's tagged-enum encoding parses correctly even
230        // when the rule_tag/rule_explanation/position siblings are
231        // present.
232        let mut value = serde_json::Value::deserialize(deserializer)?;
233        let position = value
234            .as_object_mut()
235            .and_then(|o| o.remove("position"))
236            .and_then(|p| serde_json::from_value::<Position>(p).ok());
237        if let Some(o) = value.as_object_mut() {
238            o.remove("rule_tag");
239            o.remove("rule_explanation");
240        }
241        let error: TypeError =
242            serde_json::from_value(value).map_err(serde::de::Error::custom)?;
243        Ok(PositionedError { error, position })
244    }
245}
246
247impl PositionedError {
248    pub fn new(error: TypeError, position: Option<Position>) -> Self {
249        Self { error, position }
250    }
251
252    pub fn without_position(error: TypeError) -> Self {
253        Self { error, position: None }
254    }
255
256    pub fn node(&self) -> &str {
257        self.error.node()
258    }
259}
260
261impl std::fmt::Display for PositionedError {
262    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
263        match &self.position {
264            Some(p) => write!(f, "[{}] {}", p.render(), self.error),
265            None => self.error.fmt(f),
266        }
267    }
268}
269
270impl std::error::Error for PositionedError {}
271
272impl From<TypeError> for PositionedError {
273    fn from(e: TypeError) -> Self {
274        Self::without_position(e)
275    }
276}