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}