Skip to main content

lean_rs_worker_parent/
session.rs

1//! Worker-host adapter over the process-boundary supervisor.
2//!
3//! This module is intentionally narrower than `lean-rs-host::LeanSession`.
4//! It exposes serializable outcomes that make sense across a child process:
5//! declaration text, elaboration diagnostics, and kernel-check status. Runtime
6//! handles such as `LeanExpr` and `LeanEvidence` stay inside the child.
7
8use std::collections::BTreeMap;
9use std::fmt;
10use std::marker::PhantomData;
11use std::path::PathBuf;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::time::{Duration, Instant};
15
16use serde::Serialize;
17use serde::de::DeserializeOwned;
18use serde_json::Value;
19use serde_json::value::RawValue;
20
21use lean_rs_worker_protocol::protocol::{DataRow, Diagnostic, StreamSummary};
22use lean_rs_worker_protocol::types::{
23    LeanWorkerCapabilityMetadata, LeanWorkerDeclarationFilter, LeanWorkerDeclarationInspectionRequest,
24    LeanWorkerDeclarationInspectionResult, LeanWorkerDeclarationRow, LeanWorkerDeclarationSearch,
25    LeanWorkerDeclarationSearchResult, LeanWorkerDeclarationType, LeanWorkerDeclarationVerificationBatchRequest,
26    LeanWorkerDeclarationVerificationBatchResult, LeanWorkerDeclarationVerificationRequest,
27    LeanWorkerDeclarationVerificationResult, LeanWorkerDoctorReport, LeanWorkerElabOptions, LeanWorkerElabResult,
28    LeanWorkerKernelResult, LeanWorkerMetaResult, LeanWorkerMetaTransparency, LeanWorkerModuleQuery,
29    LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryOutcome, LeanWorkerModuleQuerySelector,
30    LeanWorkerModuleSnapshotCacheClearResult, LeanWorkerOutputBudgets, LeanWorkerProofAttemptRequest,
31    LeanWorkerProofAttemptResult, LeanWorkerRendered, LeanWorkerResourceExhaustedFacts, LeanWorkerSessionImportProfile,
32};
33
34use crate::supervisor::{LeanWorker, LeanWorkerError};
35
36/// Configuration for opening one host session inside a worker child.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct LeanWorkerSessionConfig {
39    project_root: PathBuf,
40    mode: LeanWorkerSessionMode,
41    imports: Vec<String>,
42    import_profile: LeanWorkerSessionImportProfile,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub(crate) enum LeanWorkerSessionMode {
47    Capability {
48        package: String,
49        lib_name: String,
50        manifest_path: Option<PathBuf>,
51    },
52    ShimsOnly,
53}
54
55impl LeanWorkerSessionConfig {
56    /// Create a session configuration for a Lake capability and import list.
57    pub fn new(
58        project_root: impl Into<PathBuf>,
59        package: impl Into<String>,
60        lib_name: impl Into<String>,
61        imports: impl IntoIterator<Item = impl Into<String>>,
62    ) -> Self {
63        Self {
64            project_root: project_root.into(),
65            mode: LeanWorkerSessionMode::Capability {
66                package: package.into(),
67                lib_name: lib_name.into(),
68                manifest_path: None,
69            },
70            imports: imports.into_iter().map(Into::into).collect(),
71            import_profile: LeanWorkerSessionImportProfile::default(),
72        }
73    }
74
75    /// Create a manifest-backed session configuration for a Lake capability.
76    #[must_use]
77    pub fn manifest_backed(
78        project_root: impl Into<PathBuf>,
79        package: impl Into<String>,
80        lib_name: impl Into<String>,
81        manifest_path: impl Into<PathBuf>,
82        imports: impl IntoIterator<Item = impl Into<String>>,
83    ) -> Self {
84        Self {
85            project_root: project_root.into(),
86            mode: LeanWorkerSessionMode::Capability {
87                package: package.into(),
88                lib_name: lib_name.into(),
89                manifest_path: Some(manifest_path.into()),
90            },
91            imports: imports.into_iter().map(Into::into).collect(),
92            import_profile: LeanWorkerSessionImportProfile::default(),
93        }
94    }
95
96    /// Create a session configuration backed only by the bundled host shims.
97    #[must_use]
98    pub fn shims_only(project_root: impl Into<PathBuf>, imports: impl IntoIterator<Item = impl Into<String>>) -> Self {
99        Self {
100            project_root: project_root.into(),
101            mode: LeanWorkerSessionMode::ShimsOnly,
102            imports: imports.into_iter().map(Into::into).collect(),
103            import_profile: LeanWorkerSessionImportProfile::default(),
104        }
105    }
106
107    /// Select the full-session import profile for this worker session.
108    #[must_use]
109    pub fn with_import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
110        self.import_profile = profile;
111        self
112    }
113
114    pub(crate) fn project_root_string(&self) -> String {
115        self.project_root.to_string_lossy().into_owned()
116    }
117
118    pub(crate) fn mode(&self) -> &LeanWorkerSessionMode {
119        &self.mode
120    }
121
122    pub(crate) fn imports(&self) -> &[String] {
123        &self.imports
124    }
125
126    /// Return the full-session import profile used when opening this config.
127    #[must_use]
128    pub fn import_profile(&self) -> LeanWorkerSessionImportProfile {
129        self.import_profile
130    }
131
132    /// Return a copy of this config with a different import list, preserving
133    /// the loading mode and import profile.
134    #[must_use]
135    pub fn with_imports(&self, imports: impl IntoIterator<Item = impl Into<String>>) -> Self {
136        Self {
137            project_root: self.project_root.clone(),
138            mode: self.mode.clone(),
139            imports: imports.into_iter().map(Into::into).collect(),
140            import_profile: self.import_profile,
141        }
142    }
143}
144
145/// Protocol/runtime facts reported by the worker child during handshake.
146///
147/// These facts describe the `lean-rs-worker` process and framing contract.
148/// They are separate from downstream capability metadata returned by
149/// `LeanWorkerSession::capability_metadata`.
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct LeanWorkerRuntimeMetadata {
152    pub worker_version: String,
153    pub protocol_version: u16,
154    pub lean_version: Option<String>,
155}
156
157/// Parent-side cancellation token for worker-session requests.
158///
159/// Cancellation is observed by the supervisor before a request is sent and at
160/// worker progress frames while a request is in flight. In-flight cancellation
161/// cycles the child process; it does not share an in-process
162/// `LeanCancellationToken` with the child.
163#[derive(Clone, Debug, Default)]
164pub struct LeanWorkerCancellationToken {
165    cancelled: Arc<AtomicBool>,
166}
167
168impl LeanWorkerCancellationToken {
169    /// Create a non-cancelled token.
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Request cancellation.
176    pub fn cancel(&self) {
177        self.cancelled.store(true, Ordering::Release);
178    }
179
180    /// Whether cancellation was requested.
181    #[must_use]
182    pub fn is_cancelled(&self) -> bool {
183        self.cancelled.load(Ordering::Acquire)
184    }
185}
186
187/// One progress event observed by the parent from a worker request.
188#[derive(Clone, Debug, Eq, PartialEq)]
189pub struct LeanWorkerProgressEvent {
190    pub phase: String,
191    pub current: u64,
192    pub total: Option<u64>,
193    pub elapsed: Duration,
194}
195
196/// Parent-side sink for worker progress events.
197pub trait LeanWorkerProgressSink: Send + Sync {
198    fn report(&self, event: LeanWorkerProgressEvent);
199}
200
201/// One downstream-owned JSON row delivered over a worker request.
202///
203/// `stream` is a caller-defined channel name. `sequence` starts at zero per
204/// stream inside one request and is assigned by `lean-rs-worker`. `payload` is
205/// owned JSON; callers may keep it after `LeanWorkerDataSink::report` returns.
206#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct LeanWorkerDataRow {
208    pub stream: String,
209    pub sequence: u64,
210    pub payload: Value,
211}
212
213impl TryFrom<DataRow> for LeanWorkerDataRow {
214    type Error = LeanWorkerError;
215
216    fn try_from(value: DataRow) -> Result<Self, Self::Error> {
217        let payload = serde_json::from_str(value.payload.get()).map_err(|err| LeanWorkerError::Protocol {
218            message: format!("worker data-row payload decode failed: {err}"),
219        })?;
220        Ok(Self {
221            stream: value.stream,
222            sequence: value.sequence,
223            payload,
224        })
225    }
226}
227
228/// Parent-side sink for downstream data rows produced by one worker request.
229///
230/// A sink is borrowed for one request. It receives owned rows and may store
231/// them immediately, before the terminal stream response arrives. Those rows
232/// are tentative until the request returns terminal success. If `report`
233/// panics, the supervisor catches the panic and returns
234/// `LeanWorkerError::DataSinkPanic`.
235pub trait LeanWorkerDataSink: Send + Sync {
236    fn report(&self, row: LeanWorkerDataRow);
237}
238
239pub(crate) struct LeanWorkerRawDataRow {
240    pub(crate) stream: String,
241    pub(crate) sequence: u64,
242    pub(crate) payload: Box<RawValue>,
243}
244
245impl From<DataRow> for LeanWorkerRawDataRow {
246    fn from(value: DataRow) -> Self {
247        Self {
248            stream: value.stream,
249            sequence: value.sequence,
250            payload: value.payload,
251        }
252    }
253}
254
255pub(crate) trait LeanWorkerRawDataSink: Send + Sync {
256    fn report(&self, row: LeanWorkerRawDataRow);
257}
258
259#[derive(Clone, Copy)]
260pub(crate) enum LeanWorkerDataSinkTarget<'a> {
261    Value(&'a dyn LeanWorkerDataSink),
262    Raw(&'a dyn LeanWorkerRawDataSink),
263}
264
265/// One diagnostic message delivered over a worker request.
266///
267/// Diagnostics are control/observability messages, not data rows. They are
268/// delivered through `LeanWorkerDiagnosticSink` so row payloads remain
269/// downstream-owned data.
270#[derive(Clone, Debug, Eq, PartialEq)]
271pub struct LeanWorkerDiagnosticEvent {
272    pub code: String,
273    pub message: String,
274}
275
276impl From<Diagnostic> for LeanWorkerDiagnosticEvent {
277    fn from(value: Diagnostic) -> Self {
278        Self {
279            code: value.code,
280            message: value.message,
281        }
282    }
283}
284
285/// Parent-side sink for diagnostics produced by one worker request.
286pub trait LeanWorkerDiagnosticSink: Send + Sync {
287    fn report(&self, diagnostic: LeanWorkerDiagnosticEvent);
288}
289
290/// Summary returned after a worker data-stream export completes.
291///
292/// Rows delivered to `LeanWorkerDataSink` are tentative until this summary is
293/// returned successfully. Downstream callers that need atomic commit should
294/// buffer rows in their sink and commit only after terminal success.
295#[derive(Clone, Debug, Eq, PartialEq)]
296pub struct LeanWorkerStreamSummary {
297    /// Total number of rows delivered to the parent before terminal success.
298    pub total_rows: u64,
299    /// Per-stream row counts assigned by `lean-rs-worker`.
300    pub per_stream_counts: BTreeMap<String, u64>,
301    /// Elapsed time measured in the child for the streaming export.
302    pub elapsed: Duration,
303    /// Optional downstream-defined terminal metadata.
304    pub metadata: Option<Value>,
305}
306
307impl From<StreamSummary> for LeanWorkerStreamSummary {
308    fn from(value: StreamSummary) -> Self {
309        Self {
310            total_rows: value.total_rows,
311            per_stream_counts: value.per_stream_counts,
312            elapsed: Duration::from_micros(value.elapsed_micros),
313            metadata: value.metadata,
314        }
315    }
316}
317
318/// A non-streaming downstream JSON command.
319///
320/// The command names a Lean export with ABI `String -> IO String`. `Req` and
321/// `Resp` are downstream-owned serde types; `lean-rs-worker` owns request
322/// transport, worker lifecycle, timeout, cancellation, and response decoding.
323pub struct LeanWorkerJsonCommand<Req, Resp> {
324    export: String,
325    _types: PhantomData<fn(&Req) -> Resp>,
326}
327
328impl<Req, Resp> LeanWorkerJsonCommand<Req, Resp> {
329    /// Create a typed JSON command for one Lean export.
330    #[must_use]
331    pub fn new(export: impl Into<String>) -> Self {
332        Self {
333            export: export.into(),
334            _types: PhantomData,
335        }
336    }
337
338    /// Return the Lean export name used by this command.
339    #[must_use]
340    pub fn export(&self) -> &str {
341        &self.export
342    }
343}
344
345impl<Req, Resp> Clone for LeanWorkerJsonCommand<Req, Resp> {
346    fn clone(&self) -> Self {
347        Self {
348            export: self.export.clone(),
349            _types: PhantomData,
350        }
351    }
352}
353
354impl<Req, Resp> fmt::Debug for LeanWorkerJsonCommand<Req, Resp> {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        f.debug_struct("LeanWorkerJsonCommand")
357            .field("export", &self.export)
358            .finish()
359    }
360}
361
362impl<Req, Resp> PartialEq for LeanWorkerJsonCommand<Req, Resp> {
363    fn eq(&self, other: &Self) -> bool {
364        self.export == other.export
365    }
366}
367
368impl<Req, Resp> Eq for LeanWorkerJsonCommand<Req, Resp> {}
369
370/// A streaming downstream JSON command.
371///
372/// The command names a Lean export with ABI
373/// `String -> USize -> USize -> IO UInt8`. `Req`, `Row`, and `Summary` are
374/// downstream-owned serde types. Row and terminal-summary JSON are decoded at
375/// the parent boundary, after `lean-rs-worker` has handled process lifecycle,
376/// framing, diagnostics, timeout, cancellation, and completion.
377pub struct LeanWorkerStreamingCommand<Req, Row, Summary> {
378    export: String,
379    _types: PhantomData<fn(&Req) -> (Row, Summary)>,
380}
381
382impl<Req, Row, Summary> LeanWorkerStreamingCommand<Req, Row, Summary> {
383    /// Create a typed streaming command for one Lean export.
384    #[must_use]
385    pub fn new(export: impl Into<String>) -> Self {
386        Self {
387            export: export.into(),
388            _types: PhantomData,
389        }
390    }
391
392    /// Return the Lean export name used by this command.
393    #[must_use]
394    pub fn export(&self) -> &str {
395        &self.export
396    }
397}
398
399impl<Req, Row, Summary> Clone for LeanWorkerStreamingCommand<Req, Row, Summary> {
400    fn clone(&self) -> Self {
401        Self {
402            export: self.export.clone(),
403            _types: PhantomData,
404        }
405    }
406}
407
408impl<Req, Row, Summary> fmt::Debug for LeanWorkerStreamingCommand<Req, Row, Summary> {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        f.debug_struct("LeanWorkerStreamingCommand")
411            .field("export", &self.export)
412            .finish()
413    }
414}
415
416impl<Req, Row, Summary> PartialEq for LeanWorkerStreamingCommand<Req, Row, Summary> {
417    fn eq(&self, other: &Self) -> bool {
418        self.export == other.export
419    }
420}
421
422impl<Req, Row, Summary> Eq for LeanWorkerStreamingCommand<Req, Row, Summary> {}
423
424/// One typed downstream row decoded from a worker data row.
425#[derive(Clone, Debug, Eq, PartialEq)]
426pub struct LeanWorkerTypedDataRow<Row> {
427    pub stream: String,
428    pub sequence: u64,
429    pub payload: Row,
430}
431
432/// Parent-side sink for typed downstream data rows produced by one command.
433///
434/// The sink remains request-local. It sees rows immediately, before terminal
435/// success. A panic from `report` is contained by the worker supervisor and
436/// returned as `LeanWorkerError::DataSinkPanic`.
437pub trait LeanWorkerTypedDataSink<Row>: Send + Sync {
438    fn report(&self, row: LeanWorkerTypedDataRow<Row>);
439}
440
441/// Typed summary returned after a streaming command reaches terminal success.
442///
443/// Rows delivered to a typed sink remain tentative until this summary is
444/// returned. `metadata` is decoded from the downstream terminal JSON metadata,
445/// when the export provides it.
446#[derive(Clone, Debug, Eq, PartialEq)]
447pub struct LeanWorkerTypedStreamSummary<Summary> {
448    pub total_rows: u64,
449    pub per_stream_counts: BTreeMap<String, u64>,
450    pub elapsed: Duration,
451    pub metadata: Option<Summary>,
452}
453
454/// Narrow host-session adapter over a live `LeanWorker`.
455///
456/// Dropping this value does not stop the worker. If a request is cancelled
457/// while in flight, the supervisor cycles the child process and this session is
458/// invalidated; open a fresh session before issuing more host requests.
459pub struct LeanWorkerSession<'worker> {
460    worker: &'worker mut LeanWorker,
461    open: bool,
462}
463
464impl LeanWorker {
465    /// Open a host session inside the worker child.
466    ///
467    /// # Errors
468    ///
469    /// Returns `LeanWorkerError` if the worker is dead, the child cannot open
470    /// the Lake project/capability/imports, cancellation is already requested,
471    /// or protocol communication fails.
472    pub fn open_session<'worker>(
473        &'worker mut self,
474        config: &LeanWorkerSessionConfig,
475        cancellation: Option<&LeanWorkerCancellationToken>,
476        progress: Option<&dyn LeanWorkerProgressSink>,
477    ) -> Result<LeanWorkerSession<'worker>, LeanWorkerError> {
478        self.open_worker_session(config, cancellation, progress)?;
479        Ok(LeanWorkerSession {
480            worker: self,
481            open: true,
482        })
483    }
484
485    pub(crate) fn attach_open_session(&mut self) -> LeanWorkerSession<'_> {
486        LeanWorkerSession {
487            worker: self,
488            open: true,
489        }
490    }
491}
492
493impl LeanWorkerSession<'_> {
494    /// Return the timeout used for subsequent requests on this session.
495    #[must_use]
496    pub fn request_timeout(&self) -> Duration {
497        self.worker.request_timeout()
498    }
499
500    /// Change the timeout for subsequent requests on this session.
501    ///
502    /// A timeout is parent-enforced. If it fires, the supervisor kills and
503    /// replaces the child process and invalidates this session.
504    pub fn set_request_timeout(&mut self, timeout: Duration) {
505        self.worker.set_request_timeout(timeout);
506    }
507
508    /// Elaborate one term and return only process-safe success/diagnostic data.
509    ///
510    /// # Errors
511    ///
512    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
513    /// host error, cancellation is observed, a progress sink panics, or protocol
514    /// communication fails.
515    pub fn elaborate(
516        &mut self,
517        source: &str,
518        options: &LeanWorkerElabOptions,
519        cancellation: Option<&LeanWorkerCancellationToken>,
520        progress: Option<&dyn LeanWorkerProgressSink>,
521    ) -> Result<LeanWorkerElabResult, LeanWorkerError> {
522        self.with_session(|worker| worker.worker_elaborate(source, options, cancellation, progress))
523    }
524
525    /// Kernel-check one declaration and return only process-safe status/diagnostics.
526    ///
527    /// # Errors
528    ///
529    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
530    /// host error, cancellation is observed, a progress sink panics, or protocol
531    /// communication fails.
532    pub fn kernel_check(
533        &mut self,
534        source: &str,
535        options: &LeanWorkerElabOptions,
536        cancellation: Option<&LeanWorkerCancellationToken>,
537        progress: Option<&dyn LeanWorkerProgressSink>,
538    ) -> Result<LeanWorkerKernelResult, LeanWorkerError> {
539        self.with_session(|worker| worker.worker_kernel_check(source, options, cancellation, progress))
540    }
541
542    /// Query declaration kinds in bulk.
543    ///
544    /// # Errors
545    ///
546    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
547    /// host error, cancellation is observed, a progress sink panics, or protocol
548    /// communication fails.
549    pub fn declaration_kinds(
550        &mut self,
551        names: &[&str],
552        cancellation: Option<&LeanWorkerCancellationToken>,
553        progress: Option<&dyn LeanWorkerProgressSink>,
554    ) -> Result<Vec<String>, LeanWorkerError> {
555        self.with_session(|worker| worker.worker_declaration_kinds(names, cancellation, progress))
556    }
557
558    /// Render declaration names in bulk.
559    ///
560    /// # Errors
561    ///
562    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
563    /// host error, cancellation is observed, a progress sink panics, or protocol
564    /// communication fails.
565    pub fn declaration_names(
566        &mut self,
567        names: &[&str],
568        cancellation: Option<&LeanWorkerCancellationToken>,
569        progress: Option<&dyn LeanWorkerProgressSink>,
570    ) -> Result<Vec<String>, LeanWorkerError> {
571        self.with_session(|worker| worker.worker_declaration_names(names, cancellation, progress))
572    }
573
574    /// Elaborate `source` and infer the resulting expression's type.
575    ///
576    /// The child attempts notation-aware rendering via the optional
577    /// `meta_pp_expr` shim (`Lean.PrettyPrinter.ppExpr`) and falls back to
578    /// `Expr.toString` when the shim is absent or reports `Unsupported`. The
579    /// returned [`LeanWorkerRendered::rendering`] reports which path produced
580    /// the value.
581    ///
582    /// Heartbeat budgeting: each `MetaM` pass (the primary `inferType` call
583    /// and the pretty-printer) runs under the same
584    /// [`LeanWorkerElabOptions::heartbeat_limit`] value, independently
585    /// bounded—the pretty-printer does not consume budget left over from
586    /// the primary call. A `Failed` or `TimeoutOrHeartbeat` reported by the
587    /// pretty-printer surfaces as the *whole* call's failure (matching
588    /// in-process behaviour); there is no path that returns the inferred
589    /// expression alongside a pretty-printer failure. Only `Unsupported`
590    /// from `pp_expr` triggers the raw fallback.
591    ///
592    /// # Errors
593    ///
594    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
595    /// host error, cancellation is observed, a progress sink panics, or
596    /// protocol communication fails. Lean-side failures (type errors,
597    /// heartbeat exhaustion, missing capability) surface inside the returned
598    /// [`LeanWorkerMetaResult`] rather than as `Err`.
599    pub fn infer_type(
600        &mut self,
601        source: &str,
602        options: &LeanWorkerElabOptions,
603        cancellation: Option<&LeanWorkerCancellationToken>,
604        progress: Option<&dyn LeanWorkerProgressSink>,
605    ) -> Result<LeanWorkerMetaResult<LeanWorkerRendered>, LeanWorkerError> {
606        self.with_session(|worker| worker.worker_infer_type(source, options, cancellation, progress))
607    }
608
609    /// Elaborate `source` and reduce it to weak head normal form.
610    ///
611    /// Rendering and heartbeat-budgeting semantics match [`Self::infer_type`]:
612    /// the child attempts notation-aware rendering via `meta_pp_expr` and
613    /// falls back to `Expr.toString` when the shim reports `Unsupported`.
614    /// Each `MetaM` pass is independently bounded by `heartbeat_limit`; a
615    /// `Failed` or `TimeoutOrHeartbeat` from the pretty-printer surfaces as
616    /// the whole call's failure.
617    ///
618    /// # Errors
619    ///
620    /// Returns `LeanWorkerError` under the same conditions as
621    /// [`Self::infer_type`]. Lean-side failures surface inside the returned
622    /// [`LeanWorkerMetaResult`].
623    pub fn whnf(
624        &mut self,
625        source: &str,
626        options: &LeanWorkerElabOptions,
627        cancellation: Option<&LeanWorkerCancellationToken>,
628        progress: Option<&dyn LeanWorkerProgressSink>,
629    ) -> Result<LeanWorkerMetaResult<LeanWorkerRendered>, LeanWorkerError> {
630        self.with_session(|worker| worker.worker_whnf(source, options, cancellation, progress))
631    }
632
633    /// Elaborate `lhs` and `rhs` and ask Lean whether they are definitionally
634    /// equal at the supplied transparency.
635    ///
636    /// # Errors
637    ///
638    /// Returns `LeanWorkerError` under the same conditions as
639    /// [`Self::infer_type`]. Lean-side failures surface inside the returned
640    /// [`LeanWorkerMetaResult`].
641    pub fn is_def_eq(
642        &mut self,
643        lhs: &str,
644        rhs: &str,
645        transparency: LeanWorkerMetaTransparency,
646        options: &LeanWorkerElabOptions,
647        cancellation: Option<&LeanWorkerCancellationToken>,
648        progress: Option<&dyn LeanWorkerProgressSink>,
649    ) -> Result<LeanWorkerMetaResult<bool>, LeanWorkerError> {
650        self.with_session(|worker| worker.worker_is_def_eq(lhs, rhs, transparency, options, cancellation, progress))
651    }
652
653    /// Describe a declaration: its kind, rendered type, and source range.
654    ///
655    /// Returns `Ok(None)` when the name is not in the session's open
656    /// environment.
657    ///
658    /// # Errors
659    ///
660    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
661    /// host error, cancellation is observed, a progress sink panics, or
662    /// protocol communication fails.
663    pub fn describe(
664        &mut self,
665        name: &str,
666        cancellation: Option<&LeanWorkerCancellationToken>,
667        progress: Option<&dyn LeanWorkerProgressSink>,
668    ) -> Result<Option<LeanWorkerDeclarationRow>, LeanWorkerError> {
669        self.with_session(|worker| worker.worker_describe(name, cancellation, progress))
670    }
671
672    /// Search declarations and return bounded metadata-only rows plus facts.
673    ///
674    /// The worker applies structured name, kind, required-constant,
675    /// conclusion-head, and scope filters inside Lean while scanning the
676    /// imported environment. Rows are capped by `search.limit` (clamped to
677    /// `1..=100` in the child) and contain no type signatures; use
678    /// [`Self::declaration_type`] for explicit one-name type rendering.
679    ///
680    /// # Errors
681    ///
682    /// Returns `LeanWorkerError` under the same conditions as
683    /// [`Self::describe`].
684    pub fn search_declarations(
685        &mut self,
686        search: &LeanWorkerDeclarationSearch,
687        cancellation: Option<&LeanWorkerCancellationToken>,
688        progress: Option<&dyn LeanWorkerProgressSink>,
689    ) -> Result<LeanWorkerDeclarationSearchResult, LeanWorkerError> {
690        self.with_session(|worker| worker.worker_search_declarations(search, cancellation, progress))
691    }
692
693    /// Render one declaration type under a byte cap.
694    ///
695    /// The returned type text is never longer than `max_bytes`, except that
696    /// the worker also applies a 64 KiB upper ceiling. Passing `0` requests an
697    /// empty truncated rendering.
698    ///
699    /// # Errors
700    ///
701    /// Returns `LeanWorkerError` under the same conditions as
702    /// [`Self::describe`].
703    pub fn declaration_type(
704        &mut self,
705        name: &str,
706        max_bytes: usize,
707        cancellation: Option<&LeanWorkerCancellationToken>,
708        progress: Option<&dyn LeanWorkerProgressSink>,
709    ) -> Result<Option<LeanWorkerDeclarationType>, LeanWorkerError> {
710        self.with_session(|worker| worker.worker_declaration_type(name, max_bytes, cancellation, progress))
711    }
712
713    /// Inspect one selected declaration under explicit output budgets.
714    ///
715    /// Declaration search intentionally returns metadata-only rows. Use this
716    /// method after selecting one declaration name whose rendered statement
717    /// and docstring are worth paying for.
718    ///
719    /// # Errors
720    ///
721    /// Returns `LeanWorkerError` under the same conditions as
722    /// [`Self::describe`].
723    pub fn inspect_declaration(
724        &mut self,
725        request: &LeanWorkerDeclarationInspectionRequest,
726        cancellation: Option<&LeanWorkerCancellationToken>,
727        progress: Option<&dyn LeanWorkerProgressSink>,
728    ) -> Result<LeanWorkerDeclarationInspectionResult, LeanWorkerError> {
729        self.with_session(|worker| worker.worker_inspect_declaration(request, cancellation, progress))
730    }
731
732    /// Try proof snippets against an in-memory source overlay.
733    ///
734    /// The worker never writes source files. Each candidate returns a normal
735    /// status row, including failed proofs and budget exhaustion.
736    ///
737    /// # Errors
738    ///
739    /// Returns `LeanWorkerError` under the same conditions as
740    /// [`Self::process_module_query_batch`].
741    pub fn attempt_proof(
742        &mut self,
743        request: &LeanWorkerProofAttemptRequest,
744        options: &LeanWorkerElabOptions,
745        cancellation: Option<&LeanWorkerCancellationToken>,
746        progress: Option<&dyn LeanWorkerProgressSink>,
747    ) -> Result<LeanWorkerProofAttemptResult, LeanWorkerError> {
748        self.with_session(|worker| worker.worker_attempt_proof(request, options, cancellation, progress))
749    }
750
751    /// Verify one declaration in an in-memory source snapshot.
752    ///
753    /// Policy failures, missing declarations, and unsupported shim support are
754    /// returned as structured result statuses rather than worker errors.
755    ///
756    /// # Errors
757    ///
758    /// Returns `LeanWorkerError` under the same conditions as
759    /// [`Self::process_module_query_batch`].
760    pub fn verify_declaration(
761        &mut self,
762        request: &LeanWorkerDeclarationVerificationRequest,
763        options: &LeanWorkerElabOptions,
764        cancellation: Option<&LeanWorkerCancellationToken>,
765        progress: Option<&dyn LeanWorkerProgressSink>,
766    ) -> Result<LeanWorkerDeclarationVerificationResult, LeanWorkerError> {
767        self.with_session(|worker| worker.worker_verify_declaration(request, options, cancellation, progress))
768    }
769
770    /// Verify several declarations in one in-memory source snapshot.
771    ///
772    /// The whole batch is one worker request for lifecycle, timeout, and
773    /// progress purposes. Header/import state is returned once at the batch
774    /// level; rows preserve the input target order.
775    ///
776    /// # Errors
777    ///
778    /// Returns `LeanWorkerError` under the same conditions as
779    /// [`Self::verify_declaration`].
780    pub fn verify_declaration_batch(
781        &mut self,
782        request: &LeanWorkerDeclarationVerificationBatchRequest,
783        options: &LeanWorkerElabOptions,
784        cancellation: Option<&LeanWorkerCancellationToken>,
785        progress: Option<&dyn LeanWorkerProgressSink>,
786    ) -> Result<LeanWorkerDeclarationVerificationBatchResult, LeanWorkerError> {
787        self.with_session(|worker| worker.worker_verify_declaration_batch(request, options, cancellation, progress))
788    }
789
790    /// Enumerate the session's open environment and return the matching
791    /// declaration names as dotted strings.
792    ///
793    /// The child streams names one per protocol frame so total payload size
794    /// is unbounded; any single Lean name fits well under the per-frame cap.
795    ///
796    /// # Errors
797    ///
798    /// Returns `LeanWorkerError` under the same conditions as
799    /// [`Self::describe`].
800    pub fn list_declarations_strings(
801        &mut self,
802        filter: &LeanWorkerDeclarationFilter,
803        cancellation: Option<&LeanWorkerCancellationToken>,
804        progress: Option<&dyn LeanWorkerProgressSink>,
805    ) -> Result<Vec<String>, LeanWorkerError> {
806        self.with_session(|worker| worker.worker_list_declarations_strings(*filter, cancellation, progress))
807    }
808
809    /// Describe a batch of declarations in one IPC round-trip.
810    ///
811    /// Each input name produces one row in the returned vector, in the same
812    /// order. Absent names keep their slot with `kind == "missing"`,
813    /// `type_signature: None`, and `source: None` so callers can correlate
814    /// rows back to inputs positionally.
815    ///
816    /// # Errors
817    ///
818    /// Returns `LeanWorkerError` under the same conditions as
819    /// [`Self::describe`].
820    pub fn describe_bulk(
821        &mut self,
822        names: &[&str],
823        cancellation: Option<&LeanWorkerCancellationToken>,
824        progress: Option<&dyn LeanWorkerProgressSink>,
825    ) -> Result<Vec<LeanWorkerDeclarationRow>, LeanWorkerError> {
826        self.with_session(|worker| worker.worker_describe_bulk(names, cancellation, progress))
827    }
828
829    /// Parse and elaborate a Lean module, returning only the requested
830    /// bounded projection.
831    ///
832    /// # Errors
833    ///
834    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
835    /// host error, cancellation is observed, a progress sink panics, or
836    /// protocol communication fails. Header-parse failures, missing imports,
837    /// and missing capability shims surface as variants of
838    /// [`LeanWorkerModuleQueryOutcome`].
839    pub fn process_module_query(
840        &mut self,
841        source: &str,
842        query: LeanWorkerModuleQuery,
843        options: &LeanWorkerElabOptions,
844        cancellation: Option<&LeanWorkerCancellationToken>,
845        progress: Option<&dyn LeanWorkerProgressSink>,
846    ) -> Result<LeanWorkerModuleQueryOutcome, LeanWorkerError> {
847        self.with_session(|worker| worker.worker_process_module_query(source, query, options, cancellation, progress))
848    }
849
850    /// Parse and elaborate a Lean module once, returning several bounded
851    /// selector projections keyed by selector id.
852    ///
853    /// # Errors
854    ///
855    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
856    /// host error, cancellation is observed, a progress sink panics, or
857    /// protocol communication fails. Header-parse failures, missing imports,
858    /// selector unavailability, budget exhaustion, and missing capability
859    /// shims surface in the returned [`LeanWorkerModuleQueryBatchOutcome`].
860    pub fn process_module_query_batch(
861        &mut self,
862        source: &str,
863        selectors: &[LeanWorkerModuleQuerySelector],
864        budgets: &LeanWorkerOutputBudgets,
865        options: &LeanWorkerElabOptions,
866        cancellation: Option<&LeanWorkerCancellationToken>,
867        progress: Option<&dyn LeanWorkerProgressSink>,
868    ) -> Result<LeanWorkerModuleQueryBatchOutcome, LeanWorkerError> {
869        self.with_session(|worker| {
870            worker.worker_process_module_query_batch(source, selectors, budgets, options, cancellation, progress)
871        })
872    }
873
874    /// Clear the worker child's private module snapshot cache.
875    ///
876    /// # Errors
877    ///
878    /// Returns `LeanWorkerError` if the worker is dead, no session is open,
879    /// cancellation is observed, or protocol communication fails.
880    pub fn clear_module_snapshot_cache(
881        &mut self,
882        cancellation: Option<&LeanWorkerCancellationToken>,
883        progress: Option<&dyn LeanWorkerProgressSink>,
884    ) -> Result<LeanWorkerModuleSnapshotCacheClearResult, LeanWorkerError> {
885        self.with_session(|worker| worker.worker_clear_module_snapshot_cache(cancellation, progress))
886    }
887
888    /// Run a downstream streaming export and deliver JSON rows to `rows`.
889    ///
890    /// The Lean export must have ABI
891    /// `String -> USize -> USize -> IO UInt8`. The child supplies the
892    /// callback handle and trampoline; the parent only sees validated
893    /// `LeanWorkerDataRow` values. Rows are delivered immediately and become
894    /// committed only if this method returns terminal success.
895    ///
896    /// # Errors
897    ///
898    /// Returns `LeanWorkerError` if the worker is dead, the child reports a
899    /// host or stream error, cancellation is observed, a sink panics, or
900    /// protocol communication fails. In-flight cancellation cycles the child
901    /// and invalidates this session.
902    pub fn run_data_stream(
903        &mut self,
904        export: &str,
905        request: &Value,
906        rows: &dyn LeanWorkerDataSink,
907        diagnostics: Option<&dyn LeanWorkerDiagnosticSink>,
908        cancellation: Option<&LeanWorkerCancellationToken>,
909        progress: Option<&dyn LeanWorkerProgressSink>,
910    ) -> Result<LeanWorkerStreamSummary, LeanWorkerError> {
911        self.with_session(|worker| {
912            worker.worker_run_data_stream(export, request, rows, diagnostics, cancellation, progress)
913        })
914    }
915
916    fn run_data_stream_raw(
917        &mut self,
918        export: &str,
919        request: &Value,
920        rows: &dyn LeanWorkerRawDataSink,
921        diagnostics: Option<&dyn LeanWorkerDiagnosticSink>,
922        cancellation: Option<&LeanWorkerCancellationToken>,
923        progress: Option<&dyn LeanWorkerProgressSink>,
924    ) -> Result<LeanWorkerStreamSummary, LeanWorkerError> {
925        self.with_session(|worker| {
926            worker.worker_run_data_stream_raw(export, request, rows, diagnostics, cancellation, progress)
927        })
928    }
929
930    /// Run a typed non-streaming downstream JSON command.
931    ///
932    /// The Lean export must have ABI `String -> IO String`. The request is
933    /// serialized from `Req`; the returned JSON string is decoded into `Resp`.
934    /// Use this for commands that return one terminal JSON value and no rows.
935    ///
936    /// # Errors
937    ///
938    /// Returns `LeanWorkerError` if request encoding fails, the worker is
939    /// dead, the session was invalidated, the export is missing, response
940    /// decoding fails, cancellation or timeout is observed, a progress sink
941    /// panics, or protocol communication fails.
942    pub fn run_json_command<Req, Resp>(
943        &mut self,
944        command: &LeanWorkerJsonCommand<Req, Resp>,
945        request: &Req,
946        cancellation: Option<&LeanWorkerCancellationToken>,
947        progress: Option<&dyn LeanWorkerProgressSink>,
948    ) -> Result<Resp, LeanWorkerError>
949    where
950        Req: Serialize,
951        Resp: DeserializeOwned,
952    {
953        let request_json =
954            serde_json::to_string(request).map_err(|err| LeanWorkerError::TypedCommandRequestEncode {
955                export: command.export().to_owned(),
956                message: err.to_string(),
957            })?;
958        let response_json = self.with_session(|worker| {
959            worker.worker_json_command(command.export(), request_json, cancellation, progress)
960        })?;
961        serde_json::from_str(&response_json).map_err(|err| LeanWorkerError::TypedCommandResponseDecode {
962            export: command.export().to_owned(),
963            message: err.to_string(),
964        })
965    }
966
967    /// Run a typed downstream streaming command.
968    ///
969    /// The Lean export must have ABI
970    /// `String -> USize -> USize -> IO UInt8`. The request is serialized from
971    /// `Req`; each row payload is decoded into `Row`; terminal metadata is
972    /// decoded into `Summary` when present. Raw-row access remains available
973    /// through `run_data_stream`. Rows are visible to `rows` immediately, but
974    /// downstream callers that need atomic effects should commit them only
975    /// after this method returns `Ok`.
976    ///
977    /// # Errors
978    ///
979    /// Returns `LeanWorkerError` if request encoding fails, row or summary
980    /// decoding fails, the worker is dead, the session was invalidated, the
981    /// export fails, cancellation or timeout is observed, a sink panics, or
982    /// protocol communication fails. Row decode errors include the stream and
983    /// sequence that identified the bad payload.
984    pub fn run_streaming_command<Req, Row, Summary>(
985        &mut self,
986        command: &LeanWorkerStreamingCommand<Req, Row, Summary>,
987        request: &Req,
988        rows: &dyn LeanWorkerTypedDataSink<Row>,
989        diagnostics: Option<&dyn LeanWorkerDiagnosticSink>,
990        cancellation: Option<&LeanWorkerCancellationToken>,
991        progress: Option<&dyn LeanWorkerProgressSink>,
992    ) -> Result<LeanWorkerTypedStreamSummary<Summary>, LeanWorkerError>
993    where
994        Req: Serialize,
995        Row: DeserializeOwned,
996        Summary: DeserializeOwned,
997    {
998        let request_value =
999            serde_json::to_value(request).map_err(|err| LeanWorkerError::TypedCommandRequestEncode {
1000                export: command.export().to_owned(),
1001                message: err.to_string(),
1002            })?;
1003        let internal_cancellation = LeanWorkerCancellationToken::new();
1004        let cancellation_for_stream = cancellation.unwrap_or(&internal_cancellation);
1005        let typed_sink = TypedRawDataSink {
1006            export: command.export(),
1007            rows,
1008            cancellation: cancellation_for_stream,
1009            decode_error: std::sync::Mutex::new(None),
1010        };
1011
1012        // `run_data_stream_raw` invalidates the session on Cancelled/Timeout
1013        // via `with_session`; we only reshape the result here.
1014        match self.run_data_stream_raw(
1015            command.export(),
1016            &request_value,
1017            &typed_sink,
1018            diagnostics,
1019            Some(cancellation_for_stream),
1020            progress,
1021        ) {
1022            Ok(summary) => {
1023                if let Some(err) = typed_sink.take_decode_error() {
1024                    return Err(err);
1025                }
1026                let metadata = summary
1027                    .metadata
1028                    .map(|metadata| {
1029                        serde_json::from_value(metadata).map_err(|err| LeanWorkerError::TypedCommandSummaryDecode {
1030                            export: command.export().to_owned(),
1031                            message: err.to_string(),
1032                        })
1033                    })
1034                    .transpose()?;
1035                Ok(LeanWorkerTypedStreamSummary {
1036                    total_rows: summary.total_rows,
1037                    per_stream_counts: summary.per_stream_counts,
1038                    elapsed: summary.elapsed,
1039                    metadata,
1040                })
1041            }
1042            Err(LeanWorkerError::Cancelled { resource, .. }) => {
1043                if let Some(err) = typed_sink.take_decode_error() {
1044                    Err(err)
1045                } else {
1046                    Err(LeanWorkerError::Cancelled {
1047                        operation: "worker_run_data_stream",
1048                        resource,
1049                    })
1050                }
1051            }
1052            Err(err) => Err(err),
1053        }
1054    }
1055
1056    /// Query generic metadata from a downstream capability export.
1057    ///
1058    /// The Lean export must have ABI `String -> IO String`. The request and
1059    /// response strings are JSON, but callers receive a typed metadata
1060    /// envelope rather than private protocol frames.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns `LeanWorkerError` if the worker is dead, the session was
1065    /// invalidated, the export is missing, request or response JSON is
1066    /// malformed, cancellation or timeout is observed, a progress sink panics,
1067    /// or protocol communication fails.
1068    pub fn capability_metadata(
1069        &mut self,
1070        export: &str,
1071        request: &Value,
1072        cancellation: Option<&LeanWorkerCancellationToken>,
1073        progress: Option<&dyn LeanWorkerProgressSink>,
1074    ) -> Result<LeanWorkerCapabilityMetadata, LeanWorkerError> {
1075        self.with_session(|worker| worker.worker_capability_metadata(export, request, cancellation, progress))
1076    }
1077
1078    /// Run a generic doctor check from a downstream capability export.
1079    ///
1080    /// The Lean export must have ABI `String -> IO String`. Doctor diagnostics
1081    /// are capability-layer facts; data rows remain reserved for downstream
1082    /// streaming payloads.
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns `LeanWorkerError` if the worker is dead, the session was
1087    /// invalidated, the export is missing, request or response JSON is
1088    /// malformed, cancellation or timeout is observed, a progress sink panics,
1089    /// or protocol communication fails.
1090    pub fn capability_doctor(
1091        &mut self,
1092        export: &str,
1093        request: &Value,
1094        cancellation: Option<&LeanWorkerCancellationToken>,
1095        progress: Option<&dyn LeanWorkerProgressSink>,
1096    ) -> Result<LeanWorkerDoctorReport, LeanWorkerError> {
1097        self.with_session(|worker| worker.worker_capability_doctor(export, request, cancellation, progress))
1098    }
1099
1100    fn ensure_open(&self) -> Result<(), LeanWorkerError> {
1101        if self.open {
1102            Ok(())
1103        } else {
1104            Err(LeanWorkerError::UnsupportedRequest {
1105                operation: "worker_session_invalidated",
1106            })
1107        }
1108    }
1109
1110    /// Run an operation against the underlying worker, applying the session
1111    /// invalidation policy uniformly.
1112    ///
1113    /// Centralizes the rule "Cancelled or Timeout from the worker invalidates
1114    /// this session" so every typed method delegates here instead of repeating
1115    /// the same `match` discriminator. Adding a new terminal-failure variant
1116    /// to the invalidation set is now a one-line edit.
1117    fn with_session<T>(
1118        &mut self,
1119        f: impl FnOnce(&mut LeanWorker) -> Result<T, LeanWorkerError>,
1120    ) -> Result<T, LeanWorkerError> {
1121        self.ensure_open()?;
1122        let result = f(self.worker);
1123        if matches!(
1124            result,
1125            Err(LeanWorkerError::Cancelled { .. } | LeanWorkerError::Timeout { .. })
1126        ) {
1127            self.open = false;
1128        }
1129        result
1130    }
1131}
1132
1133struct TypedRawDataSink<'a, Row> {
1134    export: &'a str,
1135    rows: &'a dyn LeanWorkerTypedDataSink<Row>,
1136    cancellation: &'a LeanWorkerCancellationToken,
1137    decode_error: std::sync::Mutex<Option<LeanWorkerError>>,
1138}
1139
1140impl<Row> TypedRawDataSink<'_, Row> {
1141    fn take_decode_error(&self) -> Option<LeanWorkerError> {
1142        self.decode_error.lock().ok().and_then(|mut guard| guard.take())
1143    }
1144}
1145
1146impl<Row> LeanWorkerRawDataSink for TypedRawDataSink<'_, Row>
1147where
1148    Row: DeserializeOwned,
1149{
1150    fn report(&self, row: LeanWorkerRawDataRow) {
1151        match serde_json::from_str(row.payload.get()) {
1152            Ok(payload) => self.rows.report(LeanWorkerTypedDataRow {
1153                stream: row.stream,
1154                sequence: row.sequence,
1155                payload,
1156            }),
1157            Err(err) => {
1158                if let Ok(mut guard) = self.decode_error.lock() {
1159                    *guard = Some(LeanWorkerError::TypedCommandRowDecode {
1160                        export: self.export.to_owned(),
1161                        stream: row.stream,
1162                        sequence: row.sequence,
1163                        message: err.to_string(),
1164                    });
1165                }
1166                self.cancellation.cancel();
1167            }
1168        }
1169    }
1170}
1171
1172pub(crate) fn check_cancelled(
1173    operation: &'static str,
1174    token: Option<&LeanWorkerCancellationToken>,
1175) -> Result<(), LeanWorkerError> {
1176    if token.is_some_and(LeanWorkerCancellationToken::is_cancelled) {
1177        Err(LeanWorkerError::Cancelled {
1178            operation,
1179            resource: Box::new(pre_dispatch_cancelled_resource(operation)),
1180        })
1181    } else {
1182        Ok(())
1183    }
1184}
1185
1186fn pre_dispatch_cancelled_resource(operation: &'static str) -> LeanWorkerResourceExhaustedFacts {
1187    LeanWorkerResourceExhaustedFacts {
1188        cause: "worker_cancelled".to_owned(),
1189        work_entered_child: false,
1190        operation: Some(operation.to_owned()),
1191        current_rss_kib: None,
1192        limit_kib: None,
1193        import_count: None,
1194        worker_generation: None,
1195        restart_reason: None,
1196        queue_wait_ms: None,
1197        duration_ms: None,
1198        cold_open_attempts: None,
1199        cold_open_admitted: None,
1200        cold_open_refusals: None,
1201        import_like_requests: None,
1202        import_like_admitted: None,
1203        last_import_stats: None,
1204    }
1205}
1206
1207pub(crate) fn report_parent_progress(
1208    sink: Option<&dyn LeanWorkerProgressSink>,
1209    event: LeanWorkerProgressEvent,
1210) -> Result<(), LeanWorkerError> {
1211    let Some(sink) = sink else {
1212        return Ok(());
1213    };
1214    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.report(event))).map_err(|payload| {
1215        let message = if let Some(s) = payload.downcast_ref::<&str>() {
1216            (*s).to_owned()
1217        } else if let Some(s) = payload.downcast_ref::<String>() {
1218            s.clone()
1219        } else {
1220            "worker progress sink panicked".to_owned()
1221        };
1222        LeanWorkerError::ProgressPanic { message }
1223    })
1224}
1225
1226pub(crate) fn report_parent_data_row(
1227    sink: Option<LeanWorkerDataSinkTarget<'_>>,
1228    row: DataRow,
1229) -> Result<(), LeanWorkerError> {
1230    let Some(sink) = sink else {
1231        return Err(LeanWorkerError::Protocol {
1232            message: "worker sent data row for a request without a row sink".to_owned(),
1233        });
1234    };
1235    match sink {
1236        LeanWorkerDataSinkTarget::Value(sink) => {
1237            let row = LeanWorkerDataRow::try_from(row)?;
1238            report_value_data_row(sink, row)
1239        }
1240        LeanWorkerDataSinkTarget::Raw(sink) => report_raw_data_row(sink, row.into()),
1241    }
1242}
1243
1244fn report_value_data_row(sink: &dyn LeanWorkerDataSink, row: LeanWorkerDataRow) -> Result<(), LeanWorkerError> {
1245    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.report(row))).map_err(|payload| {
1246        let message = if let Some(s) = payload.downcast_ref::<&str>() {
1247            (*s).to_owned()
1248        } else if let Some(s) = payload.downcast_ref::<String>() {
1249            s.clone()
1250        } else {
1251            "worker data sink panicked".to_owned()
1252        };
1253        LeanWorkerError::DataSinkPanic { message }
1254    })
1255}
1256
1257fn report_raw_data_row(sink: &dyn LeanWorkerRawDataSink, row: LeanWorkerRawDataRow) -> Result<(), LeanWorkerError> {
1258    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.report(row))).map_err(|payload| {
1259        let message = if let Some(s) = payload.downcast_ref::<&str>() {
1260            (*s).to_owned()
1261        } else if let Some(s) = payload.downcast_ref::<String>() {
1262            s.clone()
1263        } else {
1264            "worker data sink panicked".to_owned()
1265        };
1266        LeanWorkerError::DataSinkPanic { message }
1267    })
1268}
1269
1270pub(crate) fn report_parent_diagnostic(
1271    sink: Option<&dyn LeanWorkerDiagnosticSink>,
1272    diagnostic: LeanWorkerDiagnosticEvent,
1273) -> Result<(), LeanWorkerError> {
1274    let Some(sink) = sink else {
1275        return Ok(());
1276    };
1277    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.report(diagnostic))).map_err(|payload| {
1278        let message = if let Some(s) = payload.downcast_ref::<&str>() {
1279            (*s).to_owned()
1280        } else if let Some(s) = payload.downcast_ref::<String>() {
1281            s.clone()
1282        } else {
1283            "worker diagnostic sink panicked".to_owned()
1284        };
1285        LeanWorkerError::DiagnosticSinkPanic { message }
1286    })
1287}
1288
1289pub(crate) fn elapsed_event(
1290    phase: String,
1291    current: u64,
1292    total: Option<u64>,
1293    started: Instant,
1294) -> LeanWorkerProgressEvent {
1295    LeanWorkerProgressEvent {
1296        phase,
1297        current,
1298        total,
1299        elapsed: started.elapsed(),
1300    }
1301}