Skip to main content

outl_exec/
runtime.rs

1//! The `Runtime` trait and its surrounding value types.
2//!
3//! Every backend (toy Lisp today, wasmtime-hosted interpreters
4//! tomorrow) implements [`Runtime`]. The trait is *deliberately tiny* —
5//! a single `execute(source, ctx) -> result` — so that we can swap
6//! implementations later without dragging UI code along.
7
8use std::path::PathBuf;
9use std::time::Duration;
10
11use thiserror::Error;
12
13/// A language backend.
14///
15/// The contract is intentionally Unix-y: take a `source` string, return
16/// stdout / stderr / exit-status / duration. Errors bubble up through
17/// [`ExecError`].
18///
19/// Implementations **must** honour `ctx.timeout`. If your runtime can't
20/// be cancelled cooperatively, wrap the work in
21/// [`crate::sandbox::with_timeout`] which spawns the work on a separate
22/// thread and drops the channel on overrun.
23pub trait Runtime: Send + Sync {
24    /// The fence info-string this runtime claims. Matched
25    /// case-insensitively against ` ```<lang> `.
26    fn language(&self) -> &'static str;
27
28    /// Run `source` and return what happened. Returning `Ok` with a
29    /// non-zero [`ExitStatus`] signals a *user-level* error (the script
30    /// ran but crashed); returning `Err` signals an infrastructure
31    /// error (timeout, OOM, missing toolchain).
32    fn execute(&self, source: &str, ctx: &ExecContext) -> Result<ExecOutput, ExecError>;
33
34    /// Whether blocks using this runtime should auto-run on every
35    /// page load **without** requiring the `auto-run::` block
36    /// property.
37    ///
38    /// Auto-run runtimes are also **excluded from manual `gx`
39    /// execution** — their results depend on external state (the
40    /// workspace, not just the fence body), so a manual re-run
41    /// provides no additional value over the automatic one.
42    ///
43    /// Default: `false`. The `query` runtime returns `true`.
44    fn auto_run(&self) -> bool {
45        false
46    }
47}
48
49/// Context passed to every execution.
50///
51/// We deliberately keep this small. Anything runtime-specific (env vars,
52/// preopened directories, sandbox tweaks) lives inside the runtime
53/// itself.
54#[derive(Debug, Clone)]
55pub struct ExecContext {
56    /// Workspace root — runtimes that resolve relative file references
57    /// (`include "./helper.lisp"`) start here.
58    pub workspace_root: PathBuf,
59    /// Optional content piped to the script as stdin. Future: chain
60    /// blocks via `((ref))`.
61    pub stdin: Option<String>,
62    /// Hard wall-clock limit. Past this we kill the run.
63    pub timeout: Duration,
64    /// Optional heap cap. Honoured only by runtimes that can enforce
65    /// it (wasmtime can; in-process toy interpreters can't yet).
66    pub mem_limit: Option<usize>,
67}
68
69impl Default for ExecContext {
70    fn default() -> Self {
71        Self {
72            workspace_root: PathBuf::from("."),
73            stdin: None,
74            timeout: Duration::from_secs(5),
75            mem_limit: None,
76        }
77    }
78}
79
80/// What an execution produced.
81#[derive(Debug, Clone)]
82pub struct ExecOutput {
83    /// Captured stdout.
84    pub stdout: String,
85    /// Captured stderr.
86    pub stderr: String,
87    /// Wall-clock duration of the call to `execute`.
88    pub duration: Duration,
89    /// How it ended.
90    pub exit: ExitStatus,
91    /// How the orchestrator should render this output into the
92    /// result subblock. See [`OutputFormat`].
93    pub format: OutputFormat,
94}
95
96/// How a run terminated.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum ExitStatus {
99    /// Normal completion.
100    Ok,
101    /// Script returned non-zero — user-level error, not an
102    /// infrastructure failure.
103    NonZero(i32),
104    /// Runtime trapped (panic, division by zero, etc). Message is
105    /// runtime-specific.
106    Trap(String),
107}
108
109/// How the orchestrator should render [`ExecOutput`] into a result
110/// subblock.
111///
112/// `Text` (the default) feeds `stdout` through
113/// [`render_result_body`](crate::result_block::render_result_body),
114/// producing the classic `> **result:** …` single child.
115///
116/// `Embeds` tells the orchestrator to split `stdout` into one embed
117/// reference per line and render each as a child bullet, so the result
118/// block becomes a **live view** of the referenced blocks. Used by
119/// the `query` runtime: results are `!((blk-XXXXXX))` references, not
120/// copies, so toggling a TODO on the original block is reflected
121/// everywhere it appears.
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
123pub enum OutputFormat {
124    /// Single result child with stdout as inline code or fenced block.
125    #[default]
126    Text,
127    /// One child per non-empty stdout line, each rendered as a bullet.
128    /// Lines are expected to be embed references (`!((blk-…))`).
129    Embeds,
130}
131
132/// Infrastructure-level errors.
133///
134/// User-script errors (a Lisp `(error ...)` form, a Python exception)
135/// surface as `Ok(ExecOutput { exit: NonZero | Trap, .. })`. This enum
136/// is reserved for "your sandbox didn't even get to run the code".
137#[derive(Debug, Error)]
138pub enum ExecError {
139    /// `ctx.timeout` elapsed before the script finished.
140    #[error("execution timed out after {0:?}")]
141    Timeout(Duration),
142    /// Out of memory — currently only fired by wasmtime-backed runtimes.
143    #[error("out of memory")]
144    OutOfMemory,
145    /// Language-specific parse / compile failure (e.g. malformed Lisp).
146    #[error("{0}")]
147    Language(String),
148    /// Sandbox setup failed (toolchain missing, wasm load error, ...).
149    #[error("sandbox: {0}")]
150    Sandbox(String),
151    /// I/O failure reading source or writing artifacts.
152    #[error("io: {0}")]
153    Io(#[from] std::io::Error),
154}