Skip to main content

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}