Skip to main content

lean_toolchain/
loader.rs

1//! Loader-side data types shared between `lean-rs` (runtime opener) and the
2//! worker wire protocol (parent ↔ child serialisation).
3//!
4//! These types live in `lean-toolchain` because the worker-protocol crate needs
5//! them on the wire and must not depend on `lean-rs` (which would re-link
6//! `libleanshared` into every parent process). `lean-rs` re-exports them at
7//! their historical paths (`lean_rs::module::*`) for source compatibility.
8
9use std::path::{Path, PathBuf};
10
11/// Stable preflight diagnostic codes for manifest-backed capability loading.
12///
13/// Single source of truth shared between the runtime preflight in `lean-rs`
14/// and the wire payloads in the worker-protocol crate.
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
16pub enum LeanLoaderDiagnosticCode {
17    /// The manifest path was absent, unreadable, or pointed at a missing file.
18    MissingManifest,
19    /// The manifest was not valid JSON or missed required fields.
20    MalformedManifest,
21    /// The manifest schema version is newer or otherwise unsupported.
22    UnsupportedManifestSchema,
23    /// The manifest's primary capability dylib is missing.
24    MissingPrimaryDylib,
25    /// A dependency dylib named by the manifest is missing.
26    MissingTransitiveDependency,
27    /// A dylib could not be parsed as a native object for this platform.
28    UnsupportedArchitecture,
29    /// The manifest was produced by an unsupported or mismatched Lean toolchain.
30    UnsupportedToolchainFingerprint,
31    /// A manifest appears older than the build artifact it describes.
32    StaleManifest,
33    /// The root module initializer named by the manifest is not exported.
34    MissingInitializer,
35    /// A Lean/imported symbol is not supplied by the manifest dependency set.
36    MissingImportedSymbol,
37}
38
39/// Whether an exported symbol is callable code or a Lean persistent global.
40#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
41pub enum LeanExportSymbolKind {
42    /// Symbol resolves to a function entry point.
43    Function,
44    /// Symbol resolves to a data-section `lean_object*` slot.
45    Global,
46}
47
48impl LeanExportSymbolKind {
49    /// Stable manifest spelling.
50    #[must_use]
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            Self::Function => "function",
54            Self::Global => "global",
55        }
56    }
57
58    pub(crate) fn from_str(value: &str) -> Option<Self> {
59        match value {
60            "function" => Some(Self::Function),
61            "global" => Some(Self::Global),
62            _ => None,
63        }
64    }
65}
66
67/// C ABI representation for one exported argument or result slot.
68#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
69pub enum LeanExportAbiRepr {
70    /// `lean_object*`.
71    LeanObject,
72    /// `uint8_t`.
73    U8,
74    /// `uint16_t`.
75    U16,
76    /// `uint32_t`.
77    U32,
78    /// `uint64_t`.
79    U64,
80    /// `size_t`.
81    USize,
82    /// `int8_t`.
83    I8,
84    /// `int16_t`.
85    I16,
86    /// `int32_t`.
87    I32,
88    /// `int64_t`.
89    I64,
90    /// `ssize_t`/Rust `isize`.
91    ISize,
92    /// `double`.
93    F64,
94}
95
96impl LeanExportAbiRepr {
97    /// Stable manifest spelling.
98    #[must_use]
99    pub const fn as_str(self) -> &'static str {
100        match self {
101            Self::LeanObject => "lean_object",
102            Self::U8 => "u8",
103            Self::U16 => "u16",
104            Self::U32 => "u32",
105            Self::U64 => "u64",
106            Self::USize => "usize",
107            Self::I8 => "i8",
108            Self::I16 => "i16",
109            Self::I32 => "i32",
110            Self::I64 => "i64",
111            Self::ISize => "isize",
112            Self::F64 => "f64",
113        }
114    }
115
116    pub(crate) fn from_str(value: &str) -> Option<Self> {
117        match value {
118            "lean_object" => Some(Self::LeanObject),
119            "u8" => Some(Self::U8),
120            "u16" => Some(Self::U16),
121            "u32" => Some(Self::U32),
122            "u64" => Some(Self::U64),
123            "usize" => Some(Self::USize),
124            "i8" => Some(Self::I8),
125            "i16" => Some(Self::I16),
126            "i32" => Some(Self::I32),
127            "i64" => Some(Self::I64),
128            "isize" => Some(Self::ISize),
129            "f64" => Some(Self::F64),
130            _ => None,
131        }
132    }
133}
134
135/// Ownership convention for one ABI slot.
136#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
137pub enum LeanExportOwnership {
138    /// Scalar slot with no Lean refcount transfer.
139    None,
140    /// Owned `lean_object*` reference is transferred.
141    Owned,
142}
143
144impl LeanExportOwnership {
145    /// Stable manifest spelling.
146    #[must_use]
147    pub const fn as_str(self) -> &'static str {
148        match self {
149            Self::None => "none",
150            Self::Owned => "owned",
151        }
152    }
153
154    pub(crate) fn from_str(value: &str) -> Option<Self> {
155        match value {
156            "none" => Some(Self::None),
157            "owned" => Some(Self::Owned),
158            _ => None,
159        }
160    }
161}
162
163/// ABI shape of one exported function argument.
164#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
165pub struct LeanExportArgAbi {
166    repr: LeanExportAbiRepr,
167    ownership: LeanExportOwnership,
168}
169
170impl LeanExportArgAbi {
171    /// Construct the ABI shape for a function argument slot.
172    #[must_use]
173    pub const fn new(repr: LeanExportAbiRepr, ownership: LeanExportOwnership) -> Self {
174        Self { repr, ownership }
175    }
176
177    /// C representation for this argument.
178    #[must_use]
179    pub const fn repr(self) -> LeanExportAbiRepr {
180        self.repr
181    }
182
183    /// Ownership convention for this argument.
184    #[must_use]
185    pub const fn ownership(self) -> LeanExportOwnership {
186        self.ownership
187    }
188
189    /// Encode as a manifest JSON object.
190    #[must_use]
191    pub fn to_json(self) -> serde_json::Value {
192        serde_json::json!({
193            "repr": self.repr.as_str(),
194            "ownership": self.ownership.as_str(),
195        })
196    }
197}
198
199/// How an exported result is decoded.
200#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
201pub enum LeanExportResultConvention {
202    /// Direct Lean return value.
203    Pure,
204    /// `lean_io_result_*` wrapper returned by an `IO α` export.
205    IoResult,
206}
207
208impl LeanExportResultConvention {
209    /// Stable manifest spelling.
210    #[must_use]
211    pub const fn as_str(self) -> &'static str {
212        match self {
213            Self::Pure => "pure",
214            Self::IoResult => "io_result",
215        }
216    }
217
218    pub(crate) fn from_str(value: &str) -> Option<Self> {
219        match value {
220            "pure" => Some(Self::Pure),
221            "io_result" => Some(Self::IoResult),
222            _ => None,
223        }
224    }
225}
226
227/// ABI shape of an exported result.
228#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
229pub struct LeanExportReturnAbi {
230    repr: LeanExportAbiRepr,
231    ownership: LeanExportOwnership,
232    convention: LeanExportResultConvention,
233}
234
235impl LeanExportReturnAbi {
236    /// Construct the ABI shape for an exported result slot.
237    #[must_use]
238    pub const fn new(
239        repr: LeanExportAbiRepr,
240        ownership: LeanExportOwnership,
241        convention: LeanExportResultConvention,
242    ) -> Self {
243        Self {
244            repr,
245            ownership,
246            convention,
247        }
248    }
249
250    /// C representation for this result.
251    #[must_use]
252    pub const fn repr(self) -> LeanExportAbiRepr {
253        self.repr
254    }
255
256    /// Ownership convention for this result.
257    #[must_use]
258    pub const fn ownership(self) -> LeanExportOwnership {
259        self.ownership
260    }
261
262    /// IO/result convention for this result.
263    #[must_use]
264    pub const fn convention(self) -> LeanExportResultConvention {
265        self.convention
266    }
267
268    /// Encode as a manifest JSON object.
269    #[must_use]
270    pub fn to_json(self) -> serde_json::Value {
271        serde_json::json!({
272            "repr": self.repr.as_str(),
273            "ownership": self.ownership.as_str(),
274            "convention": self.convention.as_str(),
275        })
276    }
277}
278
279/// Trusted manifest signature for one Lean export.
280#[derive(Clone, Debug, Eq, PartialEq)]
281pub struct LeanExportSignature {
282    symbol: String,
283    kind: LeanExportSymbolKind,
284    args: Vec<LeanExportArgAbi>,
285    result: LeanExportReturnAbi,
286}
287
288impl LeanExportSignature {
289    /// Construct a manifest signature for a function export.
290    #[must_use]
291    pub fn function(
292        symbol: impl Into<String>,
293        args: impl Into<Vec<LeanExportArgAbi>>,
294        result: LeanExportReturnAbi,
295    ) -> Self {
296        Self {
297            symbol: symbol.into(),
298            kind: LeanExportSymbolKind::Function,
299            args: args.into(),
300            result,
301        }
302    }
303
304    /// Construct a manifest signature for a global export.
305    #[must_use]
306    pub fn global(symbol: impl Into<String>, result: LeanExportReturnAbi) -> Self {
307        Self {
308            symbol: symbol.into(),
309            kind: LeanExportSymbolKind::Global,
310            args: Vec::new(),
311            result,
312        }
313    }
314
315    /// Exported symbol name.
316    #[must_use]
317    pub fn symbol(&self) -> &str {
318        &self.symbol
319    }
320
321    /// Function/global classification.
322    #[must_use]
323    pub const fn kind(&self) -> LeanExportSymbolKind {
324        self.kind
325    }
326
327    /// Argument ABI slots.
328    #[must_use]
329    pub fn args(&self) -> &[LeanExportArgAbi] {
330        &self.args
331    }
332
333    /// Result ABI slot.
334    #[must_use]
335    pub const fn result(&self) -> LeanExportReturnAbi {
336        self.result
337    }
338
339    /// Encode as a manifest JSON object.
340    #[must_use]
341    pub fn to_json(&self) -> serde_json::Value {
342        serde_json::json!({
343            "symbol": self.symbol,
344            "kind": self.kind.as_str(),
345            "args": self.args.iter().map(|arg| arg.to_json()).collect::<Vec<_>>(),
346            "return": self.result.to_json(),
347        })
348    }
349}
350
351impl LeanLoaderDiagnosticCode {
352    /// Stable string identifier suitable for logs and support reports.
353    #[must_use]
354    pub const fn as_str(self) -> &'static str {
355        match self {
356            Self::MissingManifest => "lean_rs.loader.missing_manifest",
357            Self::MalformedManifest => "lean_rs.loader.malformed_manifest",
358            Self::UnsupportedManifestSchema => "lean_rs.loader.unsupported_manifest_schema",
359            Self::MissingPrimaryDylib => "lean_rs.loader.missing_primary_dylib",
360            Self::MissingTransitiveDependency => "lean_rs.loader.missing_transitive_dependency",
361            Self::UnsupportedArchitecture => "lean_rs.loader.unsupported_architecture",
362            Self::UnsupportedToolchainFingerprint => "lean_rs.loader.unsupported_toolchain_fingerprint",
363            Self::StaleManifest => "lean_rs.loader.stale_manifest",
364            Self::MissingInitializer => "lean_rs.loader.missing_initializer",
365            Self::MissingImportedSymbol => "lean_rs.loader.missing_imported_symbol",
366        }
367    }
368}
369
370impl std::fmt::Display for LeanLoaderDiagnosticCode {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        f.write_str(self.as_str())
373    }
374}
375
376/// Severity of one loader preflight finding.
377#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
378pub enum LeanLoaderSeverity {
379    /// Informational finding that does not block loading.
380    Info,
381    /// Suspicious state that may still load.
382    Warning,
383    /// The capability should not be opened until this is fixed.
384    Error,
385}
386
387/// Maximum bytes preserved in user-facing loader diagnostic strings.
388///
389/// Matches the workspace error-message bound so diagnostics that flow back
390/// through `LeanError` are not double-truncated.
391pub const LOADER_DIAGNOSTIC_TEXT_LIMIT: usize = 4 * 1024;
392
393/// Truncate `text` to at most [`LOADER_DIAGNOSTIC_TEXT_LIMIT`] bytes on a
394/// UTF-8 char boundary.
395#[must_use]
396pub fn bound_loader_text(mut text: String) -> String {
397    if text.len() <= LOADER_DIAGNOSTIC_TEXT_LIMIT {
398        return text;
399    }
400    let mut cut = LOADER_DIAGNOSTIC_TEXT_LIMIT;
401    while cut > 0 && !text.is_char_boundary(cut) {
402        cut = cut.saturating_sub(1);
403    }
404    text.truncate(cut);
405    text
406}
407
408/// One bounded preflight finding.
409#[derive(Clone, Debug, Eq, PartialEq)]
410pub struct LeanLoaderCheck {
411    code: LeanLoaderDiagnosticCode,
412    severity: LeanLoaderSeverity,
413    subject: String,
414    message: String,
415    repair_hint: String,
416}
417
418impl LeanLoaderCheck {
419    /// Construct an `Error` finding with bounded text fields.
420    #[must_use]
421    pub fn error(
422        code: LeanLoaderDiagnosticCode,
423        subject: impl Into<String>,
424        message: impl Into<String>,
425        repair_hint: impl Into<String>,
426    ) -> Self {
427        Self {
428            code,
429            severity: LeanLoaderSeverity::Error,
430            subject: bound_loader_text(subject.into()),
431            message: bound_loader_text(message.into()),
432            repair_hint: bound_loader_text(repair_hint.into()),
433        }
434    }
435
436    /// Stable loader diagnostic code.
437    #[must_use]
438    pub fn code(&self) -> LeanLoaderDiagnosticCode {
439        self.code
440    }
441
442    /// Whether this finding blocks capability loading.
443    #[must_use]
444    pub fn severity(&self) -> LeanLoaderSeverity {
445        self.severity
446    }
447
448    /// Artifact, symbol, or manifest field this finding is about.
449    #[must_use]
450    pub fn subject(&self) -> &str {
451        &self.subject
452    }
453
454    /// Bounded explanation of the failure.
455    #[must_use]
456    pub fn message(&self) -> &str {
457        &self.message
458    }
459
460    /// Bounded repair hint for normal users.
461    #[must_use]
462    pub fn repair_hint(&self) -> &str {
463        &self.repair_hint
464    }
465}
466
467impl std::fmt::Display for LeanLoaderCheck {
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        write!(
470            f,
471            "{} [{:?}] {}: {} (repair: {})",
472            self.code.as_str(),
473            self.severity,
474            self.subject,
475            self.message,
476            self.repair_hint
477        )
478    }
479}
480
481/// Structured result of loader preflight for one capability manifest.
482#[derive(Clone, Debug, Eq, PartialEq)]
483pub struct LeanLoaderReport {
484    manifest_path: Option<PathBuf>,
485    checks: Vec<LeanLoaderCheck>,
486}
487
488impl LeanLoaderReport {
489    /// Bundle preflight findings with the manifest path they concern.
490    #[must_use]
491    pub fn new(manifest_path: Option<PathBuf>, checks: Vec<LeanLoaderCheck>) -> Self {
492        Self { manifest_path, checks }
493    }
494
495    /// Manifest path checked, if the descriptor resolved one.
496    #[must_use]
497    pub fn manifest_path(&self) -> Option<&Path> {
498        self.manifest_path.as_deref()
499    }
500
501    /// All preflight findings.
502    #[must_use]
503    pub fn checks(&self) -> &[LeanLoaderCheck] {
504        &self.checks
505    }
506
507    /// Blocking findings only.
508    pub fn errors(&self) -> impl Iterator<Item = &LeanLoaderCheck> {
509        self.checks
510            .iter()
511            .filter(|check| check.severity == LeanLoaderSeverity::Error)
512    }
513
514    /// Whether preflight found no blocking findings.
515    #[must_use]
516    pub fn is_ok(&self) -> bool {
517        self.errors().next().is_none()
518    }
519
520    /// First blocking finding, if any.
521    #[must_use]
522    pub fn first_error(&self) -> Option<&LeanLoaderCheck> {
523        self.errors().next()
524    }
525
526    /// Consume the report and return its findings.
527    #[must_use]
528    pub fn into_checks(self) -> Vec<LeanLoaderCheck> {
529        self.checks
530    }
531}
532
533/// Initializer for a Lean module hosted by a loaded dylib.
534#[derive(Clone, Debug, Eq, PartialEq)]
535pub struct LeanModuleInitializer {
536    package: String,
537    module: String,
538}
539
540impl LeanModuleInitializer {
541    /// Create an initializer descriptor from Lake package and root module names.
542    #[must_use]
543    pub fn new(package: impl Into<String>, module: impl Into<String>) -> Self {
544        Self {
545            package: package.into(),
546            module: module.into(),
547        }
548    }
549
550    /// Lake package name used by the initializer.
551    #[must_use]
552    pub fn package_name(&self) -> &str {
553        &self.package
554    }
555
556    /// Root Lean module name used by the initializer.
557    #[must_use]
558    pub fn module_name(&self) -> &str {
559        &self.module
560    }
561}
562
563/// Dependency dylib that must stay alive while a capability is loaded.
564#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct LeanLibraryDependency {
566    path: PathBuf,
567    exports_symbols_for_dependents: bool,
568    initializer: Option<LeanModuleInitializer>,
569}
570
571impl LeanLibraryDependency {
572    /// Add a dependency dylib to the bundle.
573    #[must_use]
574    pub fn path(path: impl Into<PathBuf>) -> Self {
575        Self {
576            path: path.into(),
577            exports_symbols_for_dependents: false,
578            initializer: None,
579        }
580    }
581
582    /// Make this dependency's Lean symbols available to later dylibs in the
583    /// same bundle.
584    ///
585    /// This is a capability-level requirement, not a platform-loader flag in
586    /// the public contract. On ELF platforms it maps to global symbol
587    /// visibility; other platforms use the equivalent behavior provided by the
588    /// native loader.
589    #[must_use]
590    pub fn export_symbols_for_dependents(mut self) -> Self {
591        self.exports_symbols_for_dependents = true;
592        self
593    }
594
595    /// Initialize a module from this dependency after it is opened.
596    #[must_use]
597    pub fn initializer(mut self, package: impl Into<String>, module: impl Into<String>) -> Self {
598        self.initializer = Some(LeanModuleInitializer::new(package, module));
599        self
600    }
601
602    /// On-disk path to the dependency dylib.
603    #[must_use]
604    pub fn path_ref(&self) -> &Path {
605        &self.path
606    }
607
608    /// Whether symbols from this dependency are exported to later bundle
609    /// members.
610    #[must_use]
611    pub fn exports_symbols_for_dependents(&self) -> bool {
612        self.exports_symbols_for_dependents
613    }
614
615    /// Optional module initializer for this dependency.
616    #[must_use]
617    pub fn module_initializer(&self) -> Option<&LeanModuleInitializer> {
618        self.initializer.as_ref()
619    }
620
621    /// Consume the dependency and return its module initializer, if any.
622    ///
623    /// Used by the runtime opener (`lean-rs`) to take owned ownership of the
624    /// initializer when opening the bundle, without re-cloning the strings.
625    #[must_use]
626    pub fn into_module_initializer(self) -> Option<LeanModuleInitializer> {
627        self.initializer
628    }
629}