Skip to main content

lean_rs_host/host/meta/
options.rs

1//! Bounded option bundle for [`crate::LeanSession::run_meta`].
2//!
3//! Mirrors [`crate::host::elaboration::LeanElabOptions`] in spirit: every
4//! setter saturates rather than rejecting out-of-range values, the bounds
5//! exist as safety rails the call site never has to write `.map_err`
6//! around. Parallel rather than shared because meta-services carry no
7//! source position (no `file_label`) and do carry a reducibility setting
8//! ([`LeanMetaTransparency`]) that has no analogue in elaboration.
9//!
10//! The heartbeat and diagnostic-byte ceilings reuse the existing
11//! `LEAN_HEARTBEAT_LIMIT_*` / `LEAN_DIAGNOSTIC_BYTE_LIMIT_*` constants
12//! from [`crate::host::elaboration`]—the underlying Lean machinery
13//! (`Lean.maxHeartbeats`) and the failure-bytes invariant are the same.
14
15use lean_rs::abi::traits::{IntoLean, TryFromLean, conversion_error};
16use lean_rs::error::{LeanResult, bound_message};
17use lean_rs::{LeanRuntime, Obj};
18
19use crate::host::elaboration::{
20    LEAN_DIAGNOSTIC_BYTE_LIMIT_DEFAULT, LEAN_DIAGNOSTIC_BYTE_LIMIT_MAX, LEAN_HEARTBEAT_LIMIT_DEFAULT,
21    LEAN_HEARTBEAT_LIMIT_MAX,
22};
23
24/// Reducibility setting threaded into the bounded `MetaM` runner.
25///
26/// Maps 1-1 onto Lean's `Meta.TransparencyMode` at 4.29.1. Declaration
27/// order doubles as the on-wire byte the Lean shim reads; the
28/// [`Self::as_byte`] accessor exposes that contract for the dispatch
29/// site.
30#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
31pub enum LeanMetaTransparency {
32    /// Lean's standard reducibility—non-reducible / non-irreducible
33    /// definitions unfold on demand.
34    #[default]
35    Default,
36    /// Only `@[reducible]` definitions unfold. Useful when you want
37    /// [`crate::host::meta::whnf`] to expose the surface structure of a
38    /// term without diving into expensive bodies.
39    Reducible,
40    /// `Default` plus the bodies of instance bindings.
41    Instances,
42    /// Every definition unfolds. Most aggressive setting—also the
43    /// most likely to blow the heartbeat budget on non-trivial terms.
44    All,
45}
46
47impl LeanMetaTransparency {
48    /// On-wire byte the Lean shim's `transparencyOfByte` reads.
49    #[must_use]
50    pub fn as_byte(self) -> u8 {
51        match self {
52            Self::Default => 0,
53            Self::Reducible => 1,
54            Self::Instances => 2,
55            Self::All => 3,
56        }
57    }
58
59    fn from_byte(byte: u8) -> LeanResult<Self> {
60        match byte {
61            0 => Ok(Self::Default),
62            1 => Ok(Self::Reducible),
63            2 => Ok(Self::Instances),
64            3 => Ok(Self::All),
65            other => Err(conversion_error(format!(
66                "expected LeanMetaTransparency byte 0..=3, found {other}"
67            ))),
68        }
69    }
70}
71
72impl<'lean> IntoLean<'lean> for LeanMetaTransparency {
73    fn into_lean(self, runtime: &'lean LeanRuntime) -> Obj<'lean> {
74        self.as_byte().into_lean(runtime)
75    }
76}
77
78impl<'lean> TryFromLean<'lean> for LeanMetaTransparency {
79    fn try_from_lean(obj: Obj<'lean>) -> LeanResult<Self> {
80        Self::from_byte(u8::try_from_lean(obj)?)
81    }
82}
83
84/// Bounded options threaded into [`crate::LeanSession::run_meta`].
85///
86/// Construct through [`Self::new`] or [`Default::default`] and chain
87/// the per-field builder methods. Each setter saturates at the same
88/// ceiling [`crate::LeanElabOptions`] uses; the namespace context is
89/// bounded at [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`].
90///
91/// ```ignore
92/// let opts = LeanMetaOptions::new()
93///     .heartbeat_limit(50_000)
94///     .transparency(LeanMetaTransparency::Reducible);
95/// ```
96#[derive(Clone, Debug)]
97pub struct LeanMetaOptions {
98    namespace_context: String,
99    heartbeat_limit: u64,
100    diagnostic_byte_limit: usize,
101    transparency: LeanMetaTransparency,
102}
103
104impl LeanMetaOptions {
105    /// Construct an options bundle with the documented defaults: empty
106    /// namespace context, [`LEAN_HEARTBEAT_LIMIT_DEFAULT`] heartbeats,
107    /// [`LEAN_DIAGNOSTIC_BYTE_LIMIT_DEFAULT`] bytes of diagnostics, and
108    /// [`LeanMetaTransparency::Default`] reducibility.
109    #[must_use]
110    pub fn new() -> Self {
111        Self {
112            namespace_context: String::new(),
113            heartbeat_limit: LEAN_HEARTBEAT_LIMIT_DEFAULT,
114            diagnostic_byte_limit: LEAN_DIAGNOSTIC_BYTE_LIMIT_DEFAULT,
115            transparency: LeanMetaTransparency::Default,
116        }
117    }
118
119    /// Replace the heartbeat limit. Values above
120    /// [`LEAN_HEARTBEAT_LIMIT_MAX`] saturate at the ceiling.
121    #[must_use]
122    pub fn heartbeat_limit(mut self, heartbeats: u64) -> Self {
123        self.heartbeat_limit = heartbeats.min(LEAN_HEARTBEAT_LIMIT_MAX);
124        self
125    }
126
127    /// Replace the diagnostic byte budget. Values above
128    /// [`LEAN_DIAGNOSTIC_BYTE_LIMIT_MAX`] saturate at the ceiling.
129    /// Threaded through the ABI; the current single-message failure
130    /// branches do not actively truncate (Rust's `LeanDiagnostic`
131    /// decoder already bounds at [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`]).
132    /// Multi-message services would consume the budget the same way
133    /// the elaboration shim's `serializeMessages` does.
134    #[must_use]
135    pub fn diagnostic_byte_limit(mut self, bytes: usize) -> Self {
136        self.diagnostic_byte_limit = bytes.min(LEAN_DIAGNOSTIC_BYTE_LIMIT_MAX);
137        self
138    }
139
140    /// Replace the namespace context the meta runner opens before
141    /// evaluating the action (default empty, meaning the imported
142    /// environment's root namespace). Long strings truncate at
143    /// [`lean_rs::LEAN_ERROR_MESSAGE_LIMIT`] on a UTF-8 char boundary.
144    #[must_use]
145    pub fn namespace_context(mut self, ns: &str) -> Self {
146        self.namespace_context = bound_message(ns.to_owned());
147        self
148    }
149
150    /// Replace the reducibility setting. Default is
151    /// [`LeanMetaTransparency::Default`], matching Lean's `Meta`
152    /// default.
153    #[must_use]
154    pub fn transparency(mut self, transparency: LeanMetaTransparency) -> Self {
155        self.transparency = transparency;
156        self
157    }
158
159    // -- crate-internal accessors used by the dispatch site -----------
160
161    #[allow(dead_code, reason = "reserved for run_meta dispatch accessors")]
162    pub(crate) fn namespace_context_str(&self) -> &str {
163        &self.namespace_context
164    }
165
166    pub(crate) fn heartbeats(&self) -> u64 {
167        self.heartbeat_limit
168    }
169
170    pub(crate) fn diagnostic_byte_limit_usize(&self) -> usize {
171        self.diagnostic_byte_limit
172    }
173
174    pub(crate) fn transparency_byte(&self) -> u8 {
175        self.transparency.as_byte()
176    }
177}
178
179impl Default for LeanMetaOptions {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use lean_rs::error::LEAN_ERROR_MESSAGE_LIMIT;
189
190    #[test]
191    fn defaults_match_published_constants() {
192        let opts = LeanMetaOptions::new();
193        assert_eq!(opts.heartbeats(), LEAN_HEARTBEAT_LIMIT_DEFAULT);
194        assert_eq!(opts.diagnostic_byte_limit_usize(), LEAN_DIAGNOSTIC_BYTE_LIMIT_DEFAULT);
195        assert_eq!(opts.namespace_context_str(), "");
196        assert_eq!(opts.transparency_byte(), 0);
197    }
198
199    #[test]
200    fn heartbeat_setter_saturates_at_max() {
201        let opts = LeanMetaOptions::new().heartbeat_limit(u64::MAX);
202        assert_eq!(opts.heartbeats(), LEAN_HEARTBEAT_LIMIT_MAX);
203    }
204
205    #[test]
206    fn diagnostic_byte_limit_setter_saturates_at_max() {
207        let opts = LeanMetaOptions::new().diagnostic_byte_limit(usize::MAX);
208        assert_eq!(opts.diagnostic_byte_limit_usize(), LEAN_DIAGNOSTIC_BYTE_LIMIT_MAX);
209    }
210
211    #[test]
212    fn namespace_context_bounded() {
213        let long = "x".repeat(LEAN_ERROR_MESSAGE_LIMIT * 2);
214        let opts = LeanMetaOptions::new().namespace_context(&long);
215        assert!(opts.namespace_context_str().len() <= LEAN_ERROR_MESSAGE_LIMIT);
216    }
217
218    #[test]
219    fn transparency_byte_matches_lean_constructor_order() {
220        assert_eq!(LeanMetaTransparency::Default.as_byte(), 0);
221        assert_eq!(LeanMetaTransparency::Reducible.as_byte(), 1);
222        assert_eq!(LeanMetaTransparency::Instances.as_byte(), 2);
223        assert_eq!(LeanMetaTransparency::All.as_byte(), 3);
224    }
225}