Skip to main content

parallax_core/
execution.rs

1//! Execution requests, results, and language-neutral execution state.
2
3use crate::capabilities::RuntimeCapabilities;
4use crate::ids::{ExecutionId, ObjectId};
5use crate::runtime::RuntimeKind;
6use crate::semantic::ConversionPolicy;
7use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11/// Limits applied to a single execution.
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct ExecutionLimits {
14    /// Wall-clock timeout.
15    #[serde(with = "duration_millis")]
16    pub timeout: Duration,
17    /// Maximum captured stdout+stderr bytes.
18    pub max_output_bytes: u64,
19    /// Maximum protocol message size.
20    pub max_message_bytes: u64,
21    /// Soft memory hint in bytes (enforced where supported).
22    pub max_memory_bytes: Option<u64>,
23    /// Maximum WASM fuel / instruction budget when applicable.
24    pub max_fuel: Option<u64>,
25}
26
27impl Default for ExecutionLimits {
28    fn default() -> Self {
29        Self {
30            timeout: Duration::from_secs(30),
31            max_output_bytes: 1_048_576,
32            max_message_bytes: 16_777_216,
33            max_memory_bytes: Some(256 * 1024 * 1024),
34            max_fuel: Some(10_000_000),
35        }
36    }
37}
38
39mod duration_millis {
40    use serde::{Deserialize, Deserializer, Serializer};
41    use std::time::Duration;
42
43    pub fn serialize<S>(d: &Duration, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        let ms = u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
48        serializer.serialize_u64(ms)
49    }
50
51    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
52    where
53        D: Deserializer<'de>,
54    {
55        let ms = u64::deserialize(deserializer)?;
56        Ok(Duration::from_millis(ms))
57    }
58}
59
60/// What to execute.
61#[derive(Clone, Debug, Serialize, Deserialize)]
62#[serde(tag = "kind", rename_all = "snake_case")]
63pub enum ProgramSource {
64    /// Source file on disk.
65    File {
66        /// Absolute or relative path.
67        path: String,
68    },
69    /// Inline source text.
70    Inline {
71        /// Source code.
72        source: String,
73        /// Filename hint for diagnostics.
74        filename: String,
75    },
76    /// Precompiled bytes (e.g. WASM module).
77    Bytes {
78        /// Raw module bytes (base64 in JSON).
79        #[serde(with = "serde_bytes_b64")]
80        data: Vec<u8>,
81        /// Filename hint.
82        filename: String,
83    },
84    /// Capture named bindings after evaluating a prelude snippet.
85    CaptureBindings {
86        /// Source that defines bindings.
87        source: String,
88        /// Binding names to capture into PIR.
89        names: Vec<String>,
90        /// Filename hint.
91        filename: String,
92    },
93}
94
95mod serde_bytes_b64 {
96    use serde::{Deserialize, Deserializer, Serializer};
97
98    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
99    where
100        S: Serializer,
101    {
102        use serde::ser::Error;
103        let encoded = base64_encode(bytes).map_err(S::Error::custom)?;
104        serializer.serialize_str(&encoded)
105    }
106
107    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
108    where
109        D: Deserializer<'de>,
110    {
111        use serde::de::Error;
112        let s = String::deserialize(deserializer)?;
113        base64_decode(&s).map_err(D::Error::custom)
114    }
115
116    fn base64_encode(bytes: &[u8]) -> Result<String, String> {
117        // Minimal base64 without extra dependency in core.
118        const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
119        let mut out = String::new();
120        for chunk in bytes.chunks(3) {
121            let b0 = chunk[0] as u32;
122            let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
123            let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
124            let n = (b0 << 16) | (b1 << 8) | b2;
125            out.push(TABLE[((n >> 18) & 63) as usize] as char);
126            out.push(TABLE[((n >> 12) & 63) as usize] as char);
127            out.push(if chunk.len() > 1 {
128                TABLE[((n >> 6) & 63) as usize] as char
129            } else {
130                '='
131            });
132            out.push(if chunk.len() > 2 {
133                TABLE[(n & 63) as usize] as char
134            } else {
135                '='
136            });
137        }
138        Ok(out)
139    }
140
141    fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
142        fn val(c: u8) -> Result<u8, String> {
143            match c {
144                b'A'..=b'Z' => Ok(c - b'A'),
145                b'a'..=b'z' => Ok(c - b'a' + 26),
146                b'0'..=b'9' => Ok(c - b'0' + 52),
147                b'+' => Ok(62),
148                b'/' => Ok(63),
149                _ => Err(format!("invalid base64 byte: {}", c)),
150            }
151        }
152        let bytes = input.as_bytes();
153        if bytes.len() % 4 != 0 {
154            return Err("invalid base64 length".into());
155        }
156        let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
157        for chunk in bytes.chunks(4) {
158            let n = ((val(chunk[0])? as u32) << 18)
159                | ((val(chunk[1])? as u32) << 12)
160                | ((if chunk[2] == b'=' {
161                    0
162                } else {
163                    val(chunk[2])? as u32
164                }) << 6)
165                | (if chunk[3] == b'=' {
166                    0
167                } else {
168                    val(chunk[3])? as u32
169                });
170            out.push(((n >> 16) & 0xff) as u8);
171            if chunk[2] != b'=' {
172                out.push(((n >> 8) & 0xff) as u8);
173            }
174            if chunk[3] != b'=' {
175                out.push((n & 0xff) as u8);
176            }
177        }
178        Ok(out)
179    }
180}
181
182/// Request to execute guest code.
183#[derive(Clone, Debug, Serialize, Deserialize)]
184pub struct ExecutionRequest {
185    /// Correlation id.
186    pub execution_id: ExecutionId,
187    /// Target runtime.
188    pub runtime: RuntimeKind,
189    /// Program to run.
190    pub program: ProgramSource,
191    /// Resource limits.
192    pub limits: ExecutionLimits,
193    /// Optional entry function for WASM / callable targets.
194    pub entry: Option<String>,
195    /// Optional arguments encoded as JSON for simple call conventions.
196    pub args: Vec<serde_json::Value>,
197    /// Whether to capture state after execution.
198    pub capture_state: bool,
199    /// Enable Continuum explicit-checkpoint capture (`parallax.checkpoint`).
200    #[serde(default)]
201    pub continuum: bool,
202}
203
204impl ExecutionRequest {
205    /// Execute a file with default limits.
206    pub fn file(runtime: RuntimeKind, path: impl Into<String>) -> Self {
207        Self {
208            execution_id: ExecutionId::new(),
209            runtime,
210            program: ProgramSource::File { path: path.into() },
211            limits: ExecutionLimits::default(),
212            entry: None,
213            args: Vec::new(),
214            capture_state: false,
215            continuum: false,
216        }
217    }
218}
219
220/// Outcome of an execution attempt.
221#[derive(Clone, Debug, Serialize, Deserialize)]
222pub struct ExecutionResult {
223    /// Correlation id.
224    pub execution_id: ExecutionId,
225    /// Runtime that executed.
226    pub runtime: RuntimeKind,
227    /// Whether the guest completed successfully.
228    pub success: bool,
229    /// Guest return value as JSON when applicable.
230    pub value: Option<serde_json::Value>,
231    /// Captured stdout.
232    pub stdout: String,
233    /// Captured stderr.
234    pub stderr: String,
235    /// Wall time in microseconds.
236    pub duration_us: u64,
237    /// Optional exception summary.
238    pub exception: Option<ExceptionInfo>,
239    /// Optional captured execution state (PIR-backed).
240    pub state: Option<ExecutionState>,
241    /// True when paused at an explicit Continuum safepoint.
242    #[serde(default)]
243    pub suspended: bool,
244    /// Captured Universal Execution State JSON (Continuum), when suspended.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub ues: Option<serde_json::Value>,
247    /// Safepoint report JSON when suspended.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub safepoint: Option<serde_json::Value>,
250    /// Adapter-specific stats.
251    pub stats: IndexMap<String, serde_json::Value>,
252}
253
254/// Exception / trap information.
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
256pub struct ExceptionInfo {
257    /// Exception type name.
258    pub type_name: String,
259    /// Message.
260    pub message: String,
261    /// Optional stack / traceback.
262    pub stack: Option<String>,
263}
264
265/// Language-neutral execution state.
266///
267/// Not every runtime populates every field. Consult `capabilities`.
268#[derive(Clone, Debug, Serialize, Deserialize)]
269pub struct ExecutionState {
270    /// Schema version for this state blob.
271    pub version: u32,
272    /// Origin runtime.
273    pub runtime: RuntimeKind,
274    /// Declared capabilities at capture time.
275    pub capabilities: RuntimeCapabilities,
276    /// Program identity / source reference.
277    pub program: Option<String>,
278    /// Instruction / bytecode position when known.
279    pub instruction_position: Option<String>,
280    /// Stack frames (experimental; often empty).
281    pub stack_frames: Vec<StackFrame>,
282    /// Global bindings as a PIR object graph reference root.
283    pub globals_root: Option<ObjectId>,
284    /// Local bindings root.
285    pub locals_root: Option<ObjectId>,
286    /// Heap / object graph encoded as PIR document bytes or embedded JSON.
287    /// Concrete PIR lives in `parallax-ir`; here we store a serialized document.
288    pub heap: serde_json::Value,
289    /// Pending exception if any.
290    pub pending_exception: Option<ExceptionInfo>,
291    /// Async state placeholder.
292    pub async_state: Option<serde_json::Value>,
293    /// Capability tokens granted to the execution.
294    pub granted_capabilities: Vec<String>,
295    /// Free-form runtime metadata.
296    pub runtime_metadata: IndexMap<String, serde_json::Value>,
297    /// Migration metadata.
298    pub migration_metadata: IndexMap<String, serde_json::Value>,
299    /// Conversion policy used when producing this state.
300    pub conversion_policy: ConversionPolicy,
301}
302
303impl ExecutionState {
304    /// Create an empty state shell for a runtime.
305    pub fn empty(runtime: RuntimeKind, capabilities: RuntimeCapabilities) -> Self {
306        Self {
307            version: crate::PIR_SCHEMA_VERSION,
308            runtime,
309            capabilities,
310            program: None,
311            instruction_position: None,
312            stack_frames: Vec::new(),
313            globals_root: None,
314            locals_root: None,
315            heap: serde_json::json!({}),
316            pending_exception: None,
317            async_state: None,
318            granted_capabilities: Vec::new(),
319            runtime_metadata: IndexMap::new(),
320            migration_metadata: IndexMap::new(),
321            conversion_policy: ConversionPolicy::default(),
322        }
323    }
324}
325
326/// A single stack frame when capture is available.
327#[derive(Clone, Debug, Serialize, Deserialize)]
328pub struct StackFrame {
329    /// Function name if known.
330    pub function: Option<String>,
331    /// Source location.
332    pub location: Option<String>,
333    /// Locals object id.
334    pub locals: Option<ObjectId>,
335}
336
337/// Result of restoring state into a runtime.
338#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct RestoreResult {
340    /// Whether restore succeeded.
341    pub success: bool,
342    /// Runtime that received the state.
343    pub runtime: RuntimeKind,
344    /// Warnings emitted during restore.
345    pub warnings: Vec<String>,
346    /// Bindings restored (name → summary).
347    pub restored_bindings: IndexMap<String, String>,
348    /// Duration in microseconds.
349    pub duration_us: u64,
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn default_limits_are_bounded() {
358        let limits = ExecutionLimits::default();
359        assert!(limits.timeout.as_secs() > 0);
360        assert!(limits.max_output_bytes > 0);
361    }
362}