1use crate::error::TypeError;
17
18#[derive(Debug, Clone, Copy)]
20pub struct RuleInfo {
21 pub tag: &'static str,
22 pub explanation: &'static str,
23}
24
25impl TypeError {
26 pub fn rule_tag(&self) -> &'static str {
29 match self {
30 TypeError::TypeMismatch { .. } => "type-mismatch",
31 TypeError::UnknownIdentifier { .. } => "unknown-identifier",
32 TypeError::ArityMismatch { .. } => "arity-mismatch",
33 TypeError::NonExhaustiveMatch { .. } => "non-exhaustive-match",
34 TypeError::UnknownField { .. } => "unknown-field",
35 TypeError::DuplicateField { .. } => "duplicate-field",
36 TypeError::UnknownVariant { .. } => "unknown-variant",
37 TypeError::EffectNotDeclared { .. } => "effect-not-declared",
38 TypeError::InfiniteType { .. } => "infinite-type",
39 TypeError::AmbiguousType { .. } => "ambiguous-type",
40 TypeError::RecursiveTypeWithoutConstructor { .. } => "recursive-type-without-constructor",
41 TypeError::RefinementViolation { .. } => "refinement-violation",
42 TypeError::ExamplesOnEffectfulFn { .. } => "examples-on-effectful-fn",
43 TypeError::ExampleArityMismatch { .. } => "example-arity-mismatch",
44 TypeError::ExampleMismatch { .. } => "example-mismatch",
45 }
46 }
47
48 pub fn rule_explanation(&self) -> &'static str {
52 explanation_for_tag(self.rule_tag())
53 }
54}
55
56fn explanation_for_tag(tag: &str) -> &'static str {
57 match tag {
58 "type-mismatch" => "An expression's inferred type doesn't match what the surrounding context requires \
59(return type, let-binding annotation, function argument, operator operand, etc.). Fix by changing \
60the expression to produce the expected type, or by adjusting the declared/inferred expected type \
61to match.",
62 "unknown-identifier" => "A name referenced in scope is not declared. Either the binding is missing, \
63the name is misspelled, or an `import` is missing. Check for typos first; then verify the relevant \
64`let`, parameter, or top-level `fn` is in scope.",
65 "arity-mismatch" => "A call site supplies a different number of arguments than the function or \
66constructor accepts. Either add the missing arguments or remove the extras.",
67 "non-exhaustive-match" => "A `match` expression doesn't cover every case of its scrutinee's type. \
68Add the missing arms listed in the error, or add a `_` wildcard if catching the remainder is intended.",
69 "unknown-field" => "A record field access or literal references a field name that isn't part of \
70the record type. Verify spelling and that the type really has that field — check the type declaration.",
71 "duplicate-field" => "A record literal lists the same field name twice. Each field must appear \
72exactly once. Remove the duplicate or rename one of them.",
73 "unknown-variant" => "A constructor pattern or expression references a variant name that isn't \
74part of the union type. Verify spelling and that the variant exists on this union.",
75 "effect-not-declared" => "A function body invokes an effect (io, fs_read, net, …) that the \
76function's signature doesn't declare. Either add the effect to the function's `[effects]` annotation \
77or remove the call that produces it.",
78 "infinite-type" => "Inference would require a type to contain itself (e.g. `t = List<t>` with no \
79constructor). Add a nominal type wrapper or restructure the data so the recursion is mediated by a \
80named type.",
81 "ambiguous-type" => "Inference couldn't pick a single concrete type for an expression. Add a type \
82annotation to disambiguate.",
83 "recursive-type-without-constructor" => "A type alias references itself with no constructor in \
84between, so no value of the type can ever be built. Make the recursive position carry a constructor \
85(e.g. `Cons<T, List<T>> | Nil`).",
86 "refinement-violation" => "A literal argument provably violates a refinement-type predicate \
87(#209). Adjust the argument to satisfy the predicate, or relax the predicate at the function \
88signature.",
89 "examples-on-effectful-fn" => "A function with an `examples { ... }` block (#369) also \
90declares effects. Signature-level examples are pure-only in v1 — they must be deterministic so the \
91contract is reproducible. Either remove the effects from the signature, or remove the examples block \
92and rely on external tests.",
93 "example-arity-mismatch" => "A case inside an `examples { ... }` block (#369) supplies a \
94different number of arguments than the function declares. Match the call's argument count to the \
95function's parameter count.",
96 "example-mismatch" => "A case inside an `examples { ... }` block (#369) ran successfully \
97but the function body's actual return value differs from the declared `expected` value. Either \
98update the example to match the new behavior, or fix the body to produce the declared value.",
99 _ => "Unknown rule. The rule_tag may have been introduced after this Lex release.",
100 }
101}
102
103pub fn all_rules() -> &'static [RuleInfo] {
107 &[
108 RuleInfo { tag: "type-mismatch", explanation: TYPE_MISMATCH },
109 RuleInfo { tag: "unknown-identifier", explanation: UNKNOWN_IDENT },
110 RuleInfo { tag: "arity-mismatch", explanation: ARITY_MISMATCH },
111 RuleInfo { tag: "non-exhaustive-match", explanation: NON_EXHAUSTIVE },
112 RuleInfo { tag: "unknown-field", explanation: UNKNOWN_FIELD },
113 RuleInfo { tag: "duplicate-field", explanation: DUPLICATE_FIELD },
114 RuleInfo { tag: "unknown-variant", explanation: UNKNOWN_VARIANT },
115 RuleInfo { tag: "effect-not-declared", explanation: EFFECT_NOT_DECLARED },
116 RuleInfo { tag: "infinite-type", explanation: INFINITE_TYPE },
117 RuleInfo { tag: "ambiguous-type", explanation: AMBIGUOUS_TYPE },
118 RuleInfo { tag: "recursive-type-without-constructor", explanation: RECURSIVE_NO_CTOR },
119 RuleInfo { tag: "refinement-violation", explanation: REFINEMENT_VIOLATION },
120 RuleInfo { tag: "examples-on-effectful-fn", explanation: EXAMPLES_ON_EFFECTFUL_FN },
121 RuleInfo { tag: "example-arity-mismatch", explanation: EXAMPLE_ARITY_MISMATCH },
122 RuleInfo { tag: "example-mismatch", explanation: EXAMPLE_MISMATCH },
123 ]
124}
125
126pub fn suggested_transform_for(rule_tag: &str) -> Option<serde_json::Value> {
145 let (kind_hint, summary, details) = match rule_tag {
146 "type-mismatch" => (
147 "ReplaceMatchArm",
148 "Replace the offending match arm (or expression) so its body produces the expected type.",
149 "When a function body's inferred type doesn't match its signature, the easiest \
150typed-transform fix is `ReplaceMatchArm` — rebuild whichever arm produces the wrong type so it \
151returns the expected one. For non-match expressions, the LLM-driven `lex repair --apply` flow \
152can rewrite the body via `ModifyBody`.",
153 ),
154 "unknown-identifier" => (
155 "RenameLocal",
156 "If the name is a typo, rename a similarly-spelled in-scope binding to match.",
157 "An `unknown-identifier` error is most often a typo. Search the function's lexical \
158scope for a binding whose name is a single edit away and apply `RenameLocal` to switch references. \
159If no nearby name exists, the missing binding probably needs a `let` or an `import` — fall back to \
160LLM-driven repair.",
161 ),
162 "non-exhaustive-match" => (
163 "ReplaceMatchArm",
164 "Add the missing match arms (or a `_` wildcard) covering the unhandled variants.",
165 "Use `ReplaceMatchArm` to append arms for the variants listed in the error's \
166`missing` field. If catching the remainder is intended, a single `_` wildcard arm suffices; \
167otherwise add one explicit arm per missing variant so the audit trail records the new semantics.",
168 ),
169 "effect-not-declared" => (
170 "ChangeEffectSig",
171 "Add the inferred effect to the function's `[effects]` declaration.",
172 "The function body invokes an effect that the signature doesn't declare. Either add \
173the effect to the signature via `ChangeEffectSig` (preferred — the effect is genuinely needed) or \
174remove the call that produces it via `ModifyBody` (preferred when the effect was unintentional).",
175 ),
176 "arity-mismatch" => (
177 "ModifyBody",
178 "Match the call site's argument count to the function's declared arity.",
179 "The number of arguments at the call site doesn't match the declared signature. \
180Add the missing arguments or remove the extras. No typed transform directly applies — \
181use the LLM-driven `lex repair --apply` flow with `ModifyBody` to rewrite the call site.",
182 ),
183 "unknown-field" => (
184 "ModifyBody",
185 "Verify the field spelling and the record type's declaration; rewrite the access.",
186 "The field name isn't part of the record type. Either correct the spelling or add \
187the missing field to the type declaration. Use the LLM-driven `lex repair --apply` flow with \
188`ModifyBody` to rewrite the field access once the correct name is known.",
189 ),
190 "ambiguous-type" => (
191 "ModifyBody",
192 "Add a type annotation at the ambiguous expression to disambiguate inference.",
193 "Inference couldn't pick a single concrete type. Add an explicit type annotation on \
194the offending `let` binding, function parameter, or function return type via `ModifyBody`. The \
195LLM-driven `lex repair --apply` flow can synthesize the annotation from the surrounding context.",
196 ),
197 "example-mismatch" => (
198 "ModifyBody",
199 "Reconcile the body with its declared example — fix one or the other.",
200 "An `examples { ... }` case ran but produced a value different from the declared \
201`expected`. Either the example is stale (LLM should rewrite the case via `ReplaceMatchArm` against \
202the `examples` block) or the body regressed (LLM should rewrite the body via `ModifyBody` to make \
203the case pass). The `case_index` field in the error identifies which case to act on.",
204 ),
205 _ => return None,
206 };
207 Some(serde_json::json!({
208 "kind_hint": kind_hint,
209 "rule_tag": rule_tag,
210 "summary": summary,
211 "details": details,
212 }))
213}
214
215const TYPE_MISMATCH: &str = "An expression's inferred type doesn't match what the surrounding context requires \
219(return type, let-binding annotation, function argument, operator operand, etc.). Fix by changing \
220the expression to produce the expected type, or by adjusting the declared/inferred expected type \
221to match.";
222const UNKNOWN_IDENT: &str = "A name referenced in scope is not declared. Either the binding is missing, \
223the name is misspelled, or an `import` is missing. Check for typos first; then verify the relevant \
224`let`, parameter, or top-level `fn` is in scope.";
225const ARITY_MISMATCH: &str = "A call site supplies a different number of arguments than the function or \
226constructor accepts. Either add the missing arguments or remove the extras.";
227const NON_EXHAUSTIVE: &str = "A `match` expression doesn't cover every case of its scrutinee's type. \
228Add the missing arms listed in the error, or add a `_` wildcard if catching the remainder is intended.";
229const UNKNOWN_FIELD: &str = "A record field access or literal references a field name that isn't part of \
230the record type. Verify spelling and that the type really has that field — check the type declaration.";
231const DUPLICATE_FIELD: &str = "A record literal lists the same field name twice. Each field must appear \
232exactly once. Remove the duplicate or rename one of them.";
233const UNKNOWN_VARIANT: &str = "A constructor pattern or expression references a variant name that isn't \
234part of the union type. Verify spelling and that the variant exists on this union.";
235const EFFECT_NOT_DECLARED: &str = "A function body invokes an effect (io, fs_read, net, …) that the \
236function's signature doesn't declare. Either add the effect to the function's `[effects]` annotation \
237or remove the call that produces it.";
238const INFINITE_TYPE: &str = "Inference would require a type to contain itself (e.g. `t = List<t>` with no \
239constructor). Add a nominal type wrapper or restructure the data so the recursion is mediated by a \
240named type.";
241const AMBIGUOUS_TYPE: &str = "Inference couldn't pick a single concrete type for an expression. Add a type \
242annotation to disambiguate.";
243const RECURSIVE_NO_CTOR: &str = "A type alias references itself with no constructor in \
244between, so no value of the type can ever be built. Make the recursive position carry a constructor \
245(e.g. `Cons<T, List<T>> | Nil`).";
246const REFINEMENT_VIOLATION: &str = "A literal argument provably violates a refinement-type predicate \
247(#209). Adjust the argument to satisfy the predicate, or relax the predicate at the function \
248signature.";
249const EXAMPLES_ON_EFFECTFUL_FN: &str = "A function with an `examples { ... }` block (#369) also \
250declares effects. Signature-level examples are pure-only in v1 — they must be deterministic so the \
251contract is reproducible. Either remove the effects from the signature, or remove the examples block \
252and rely on external tests.";
253const EXAMPLE_ARITY_MISMATCH: &str = "A case inside an `examples { ... }` block (#369) supplies a \
254different number of arguments than the function declares. Match the call's argument count to the \
255function's parameter count.";
256const EXAMPLE_MISMATCH: &str = "A case inside an `examples { ... }` block (#369) ran successfully \
257but the function body's actual return value differs from the declared `expected` value. Either \
258update the example to match the new behavior, or fix the body to produce the declared value.";
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn every_variant_has_a_distinct_tag() {
266 let tags: Vec<&str> = all_rules().iter().map(|r| r.tag).collect();
269 let unique: std::collections::BTreeSet<&str> = tags.iter().copied().collect();
270 assert_eq!(unique.len(), tags.len(), "rule tags must be unique: {tags:?}");
271 }
272
273 #[test]
274 fn every_variant_has_a_nonempty_explanation() {
275 for rule in all_rules() {
276 assert!(!rule.explanation.is_empty(), "rule `{}` lacks an explanation", rule.tag);
277 assert!(
278 rule.explanation.len() > 40,
279 "rule `{}` explanation is too short to be useful for LLM repair",
280 rule.tag
281 );
282 }
283 }
284
285 #[test]
286 fn type_error_methods_match_catalog() {
287 let cases: Vec<TypeError> = vec![
290 TypeError::TypeMismatch {
291 at_node: "n_0".into(),
292 expected: "Int".into(),
293 got: "Str".into(),
294 context: vec![],
295 },
296 TypeError::UnknownIdentifier { at_node: "n_0".into(), name: "x".into() },
297 TypeError::ArityMismatch { at_node: "n_0".into(), expected: 1, got: 2 },
298 TypeError::NonExhaustiveMatch { at_node: "n_0".into(), missing: vec!["None".into()] },
299 TypeError::UnknownField {
300 at_node: "n_0".into(),
301 record_type: "User".into(),
302 field: "ag".into(),
303 },
304 TypeError::DuplicateField { at_node: "n_0".into(), field: "name".into() },
305 TypeError::UnknownVariant { at_node: "n_0".into(), constructor: "Nada".into() },
306 TypeError::EffectNotDeclared { at_node: "n_0".into(), effect: "io".into() },
307 TypeError::InfiniteType { at_node: "n_0".into() },
308 TypeError::AmbiguousType { at_node: "n_0".into() },
309 TypeError::RecursiveTypeWithoutConstructor {
310 at_node: "n_0".into(),
311 name: "Bad".into(),
312 },
313 TypeError::RefinementViolation {
314 at_node: "n_0".into(),
315 fn_name: "f".into(),
316 param_index: 0,
317 binding: "x".into(),
318 reason: "x > 0".into(),
319 },
320 TypeError::ExamplesOnEffectfulFn {
321 at_node: "n_0".into(),
322 fn_name: "f".into(),
323 },
324 TypeError::ExampleArityMismatch {
325 at_node: "n_0".into(),
326 fn_name: "f".into(),
327 case_index: 0,
328 expected: 2,
329 got: 1,
330 },
331 TypeError::ExampleMismatch {
332 at_node: "n_0".into(),
333 fn_name: "f".into(),
334 case_index: 0,
335 expected: "1".into(),
336 got: "2".into(),
337 },
338 ];
339 let catalog: std::collections::BTreeMap<&str, &str> =
340 all_rules().iter().map(|r| (r.tag, r.explanation)).collect();
341 assert_eq!(cases.len(), catalog.len(), "every variant must be covered");
342 for e in &cases {
343 let tag = e.rule_tag();
344 let expl = catalog.get(tag).unwrap_or_else(|| panic!("tag `{tag}` not in catalog"));
345 assert_eq!(e.rule_explanation(), *expl, "tag/explanation mismatch on {tag}");
346 }
347 }
348
349 #[test]
350 fn suggested_transform_covers_at_least_five_rules() {
351 let mut covered = 0;
354 for rule in all_rules() {
355 if suggested_transform_for(rule.tag).is_some() {
356 covered += 1;
357 }
358 }
359 assert!(
360 covered >= 5,
361 "suggested_transform must cover ≥5 rule_tags; got {covered}"
362 );
363 }
364
365 #[test]
366 fn suggested_transform_shape_is_consistent() {
367 for rule in all_rules() {
370 let Some(s) = suggested_transform_for(rule.tag) else { continue };
371 for field in ["kind_hint", "rule_tag", "summary", "details"] {
372 assert!(
373 s.get(field).and_then(|v| v.as_str()).is_some_and(|v| !v.is_empty()),
374 "rule `{}` suggestion missing/empty `{field}`: {s}",
375 rule.tag
376 );
377 }
378 assert_eq!(
379 s.get("rule_tag").and_then(|v| v.as_str()),
380 Some(rule.tag),
381 "suggestion's rule_tag must echo the input tag"
382 );
383 }
384 }
385
386 #[test]
387 fn unknown_rule_tag_returns_none() {
388 assert!(suggested_transform_for("does-not-exist").is_none());
389 }
390}