lean_rs_host/host/elaboration/diagnostic.rs
1//! Typed diagnostic surface attached to elaboration / kernel-check
2//! failures.
3//!
4//! Lean's `MessageLog` carries a severity tag, a human-readable message,
5//! and an optional source position. The capability shim copies each
6//! diagnostic into a `Diagnostic` structure on the Lean side; the impls
7//! in this module decode them into Rust [`LeanDiagnostic`] /
8//! [`LeanPosition`] values without inspecting Lean's internal
9//! representation.
10//!
11//! The Lean shim owns the diagnostic shape. This module decodes the
12//! domain fields through `lean-rs` object views and keeps Lean runtime
13//! layout details out of the host layer.
14
15use lean_rs::Obj;
16use lean_rs::abi::nat;
17use lean_rs::abi::structure::{take_ctor_objects, view};
18use lean_rs::abi::traits::{TryFromLean, conversion_error};
19use lean_rs::error::{LeanResult, bound_message};
20
21/// Severity classification attached to each [`LeanDiagnostic`]. Mirrors
22/// Lean's `MessageSeverity` constructors at 4.29.1.
23#[derive(Copy, Clone, Debug, Eq, PartialEq)]
24pub enum LeanSeverity {
25 /// Informational diagnostic; the operation may still have succeeded.
26 Info,
27 /// Warning diagnostic; the operation may still have succeeded.
28 Warning,
29 /// Error diagnostic; the operation did not succeed.
30 Error,
31}
32
33impl LeanSeverity {
34 /// Decode the byte the Lean side wrote for the `severity` scalar field.
35 /// `0 = info`, `1 = warning`, `2 = error` per the Lean-side
36 /// declaration order of `LeanRsHostShims.Elaboration.Severity`.
37 fn from_byte(byte: u8) -> LeanResult<Self> {
38 match byte {
39 0 => Ok(Self::Info),
40 1 => Ok(Self::Warning),
41 2 => Ok(Self::Error),
42 other => Err(conversion_error(format!(
43 "Lean Severity tag {other} is not in {{0=info, 1=warning, 2=error}}"
44 ))),
45 }
46 }
47}
48
49/// Source position attached to a Lean-emitted diagnostic.
50///
51/// `line` and `column` are 1-indexed (Lean convention). `end_line` /
52/// `end_column` are present when Lean attached an end position to the
53/// diagnostic—parser errors usually carry only a start position, while
54/// elaborator and kernel errors carry both.
55#[derive(Copy, Clone, Debug, Eq, PartialEq)]
56pub struct LeanPosition {
57 line: u32,
58 column: u32,
59 end_line: Option<u32>,
60 end_column: Option<u32>,
61}
62
63impl LeanPosition {
64 /// 1-indexed line number.
65 #[must_use]
66 pub fn line(&self) -> u32 {
67 self.line
68 }
69
70 /// 1-indexed column number.
71 #[must_use]
72 pub fn column(&self) -> u32 {
73 self.column
74 }
75
76 /// 1-indexed end line, if Lean attached one.
77 #[must_use]
78 pub fn end_line(&self) -> Option<u32> {
79 self.end_line
80 }
81
82 /// 1-indexed end column, if Lean attached one.
83 #[must_use]
84 pub fn end_column(&self) -> Option<u32> {
85 self.end_column
86 }
87}
88
89impl<'lean> TryFromLean<'lean> for LeanPosition {
90 fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
91 let [line_o, column_o, end_line_o, end_column_o] = take_ctor_objects::<4>(obj, 0, "DiagnosticPos")?;
92 Ok(Self {
93 line: decode_nat_u32(line_o, "DiagnosticPos.line")?,
94 column: decode_nat_u32(column_o, "DiagnosticPos.column")?,
95 end_line: decode_option_nat_u32(end_line_o, "DiagnosticPos.endLine")?,
96 end_column: decode_option_nat_u32(end_column_o, "DiagnosticPos.endColumn")?,
97 })
98 }
99}
100
101/// One Lean-emitted diagnostic: severity tag, bounded message, optional
102/// source position, and the file label the elaborator received.
103///
104/// The `message` is structurally bounded at
105/// [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`]: the decoder truncates on a UTF-8
106/// char boundary before storing.
107#[derive(Clone, Debug)]
108pub struct LeanDiagnostic {
109 severity: LeanSeverity,
110 message: String,
111 position: Option<LeanPosition>,
112 file_label: String,
113}
114
115impl LeanDiagnostic {
116 /// The severity tag Lean attached to this diagnostic.
117 #[must_use]
118 pub fn severity(&self) -> LeanSeverity {
119 self.severity
120 }
121
122 /// Lean's diagnostic message, truncated to at most
123 /// [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`] bytes on a UTF-8 char
124 /// boundary.
125 #[must_use]
126 pub fn message(&self) -> &str {
127 &self.message
128 }
129
130 /// Source position when Lean attached one. Parser-level errors
131 /// often carry no position; elaborator and kernel errors do.
132 #[must_use]
133 pub fn position(&self) -> Option<&LeanPosition> {
134 self.position.as_ref()
135 }
136
137 /// The file label this diagnostic belongs to. Falls back to the
138 /// caller-supplied [`crate::host::elaboration::LeanElabOptions::file_label`]
139 /// when Lean's own `MessageLog` did not carry a file name.
140 #[must_use]
141 pub fn file_label(&self) -> &str {
142 &self.file_label
143 }
144
145 /// Construct a synthetic error-severity diagnostic without a source
146 /// position. Used by the host stack when it must surface a
147 /// diagnostic that did not originate in Lean's `MessageLog`—for
148 /// example, the `LeanMetaResponse::Unsupported` branch built when a
149 /// capability dylib does not export the requested meta service.
150 /// The `message` is bounded at [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`]
151 /// on a UTF-8 char boundary, mirroring the Lean-decoded path.
152 pub(crate) fn synthetic_error(message: String, file_label: String) -> Self {
153 Self {
154 severity: LeanSeverity::Error,
155 message: bound_message(message),
156 position: None,
157 file_label,
158 }
159 }
160}
161
162impl<'lean> TryFromLean<'lean> for LeanDiagnostic {
163 fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
164 let diagnostic = view(&obj).ctor_shape(0, 3, "Diagnostic")?;
165 let severity_byte = diagnostic.uint8(0, "Diagnostic.severity")?;
166 let [msg_o, pos_o, label_o] = take_ctor_objects::<3>(obj, 0, "Diagnostic")?;
167 let severity = LeanSeverity::from_byte(severity_byte)?;
168 let message = bound_message(String::try_from_lean(msg_o)?);
169 let position = Option::<LeanPosition>::try_from_lean(pos_o)?;
170 let file_label = String::try_from_lean(label_o)?;
171 Ok(Self {
172 severity,
173 message,
174 position,
175 file_label,
176 })
177 }
178}
179
180/// Decode a Lean `Nat` slot to a `u32`, refusing values that overflow.
181fn decode_nat_u32(obj: Obj<'_>, label: &str) -> LeanResult<u32> {
182 let raw = nat::try_to_u64(obj)?;
183 u32::try_from(raw).map_err(|_| conversion_error(format!("{label} = {raw} exceeds u32 range")))
184}
185
186/// Decode a Lean `Option Nat` slot, refusing wrong tags / out-of-range
187/// `Some` payloads. Inlined rather than reusing [`Option<T>`]'s blanket
188/// impl because the inner `T = Nat` does not match the polymorphic-boxed
189/// `u32::try_from_lean` ABI (Nat is scalar-tagged, `UInt32` in
190/// polymorphic position is ctor-boxed).
191fn decode_option_nat_u32(obj: Obj<'_>, label: &str) -> LeanResult<Option<u32>> {
192 let obj_view = view(&obj);
193 if obj_view.is_scalar() {
194 let payload = obj_view.scalar_payload(label)?;
195 return match payload {
196 0 => Ok(None),
197 other => Err(conversion_error(format!(
198 "{label}: expected Option.none (scalar tag 0), found scalar payload {other}"
199 ))),
200 };
201 }
202 let ctor = obj_view.ctor()?;
203 if ctor.tag() != 1 {
204 return Err(conversion_error(format!(
205 "{label}: expected Option.some ctor (tag 1), found heap tag {}",
206 ctor.tag()
207 )));
208 }
209 let [inner] = take_ctor_objects::<1>(obj, 1, "Option Nat")?;
210 Ok(Some(decode_nat_u32(inner, label)?))
211}