Skip to main content

cymule_runtime/
engine.rs

1use std::collections::BTreeMap;
2
3use cymule_core::{
4    COMMAND_VERSION, Command, CommandEnvelope, CommandReceiptStatus, DispatchPolicy,
5    EffectTransition, Expression, Machine, MutationKind, Operation, PlanCandidate, ROOT_SCOPE_ID,
6    ReconciliationResolution, Region, SealedPlan, WorldOutcome, canonical_bytes, effect_intent_id,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::{
12    PluginHost, PluginManifest, PluginRequest, PluginResponse, RuntimeError, RuntimeResult,
13};
14
15/// Result returned by one-shot embedded execution.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ExecutionResult {
19    /// Run identity.
20    pub run_id: String,
21    /// Sealed plan identity.
22    pub plan_id: String,
23    /// Typed Flow result.
24    pub value: Value,
25    /// Deterministic replay projection digest.
26    pub projection_digest: String,
27    /// Final stale-action token.
28    pub precondition_token: String,
29    /// Structural effect intent identities admitted by the run.
30    pub effects: Vec<String>,
31}
32
33#[derive(Debug)]
34struct PendingEffect {
35    intent_id: String,
36    operation: String,
37    input: Value,
38    bind: Option<String>,
39}
40
41#[derive(Debug)]
42struct RegionOutcome {
43    value: Value,
44    pending: Vec<PendingEffect>,
45}
46
47/// Synchronous reference runtime over the trusted in-memory Machine.
48pub struct EmbeddedRuntime<P: PluginHost> {
49    machine: Machine,
50    plugin: P,
51    command_sequence: u64,
52    scope_sequence: u64,
53    invocation_sequence: u64,
54}
55
56impl<P: PluginHost> EmbeddedRuntime<P> {
57    /// Construct an embedded runtime with one plugin host.
58    pub fn new(plugin: P) -> Self {
59        Self {
60            machine: Machine::new(),
61            plugin,
62            command_sequence: 0,
63            scope_sequence: 0,
64            invocation_sequence: 0,
65        }
66    }
67
68    /// Access the underlying machine for queries and conformance assertions.
69    pub const fn machine(&self) -> &Machine {
70        &self.machine
71    }
72
73    /// Seal a language-neutral candidate using the trusted Rust kernel.
74    pub fn seal(&mut self, candidate: PlanCandidate) -> RuntimeResult<SealedPlan> {
75        self.machine.seal_plan(candidate).map_err(Into::into)
76    }
77
78    /// Execute a sealed plan to a terminal Result in the Embedded profile.
79    pub fn execute(
80        &mut self,
81        plan: SealedPlan,
82        input: &Value,
83        run_id: impl Into<String>,
84    ) -> RuntimeResult<ExecutionResult> {
85        plan.verify()?;
86        self.machine.insert_plan(plan.clone())?;
87        let manifest = self.plugin.describe()?;
88        validate_manifest(&plan, &manifest)?;
89        let run_id = run_id.into();
90        let binding_context = format!("binding:plugin/{}", manifest.implementation_id);
91
92        self.submit(
93            &run_id,
94            Command::StartRun {
95                plan_id: plan.plan_id.clone(),
96                binding_context,
97            },
98        )?;
99        self.submit(
100            &run_id,
101            Command::BeginAttempt {
102                attempt_id: "attempt:root/1".to_owned(),
103                continuation_id: "continuation:root".to_owned(),
104                occurrence_binding: format!("binding:{}/runtime", manifest.implementation_id),
105                epoch: 0,
106            },
107        )?;
108
109        let definition = plan
110            .candidate
111            .definitions
112            .iter()
113            .find(|definition| definition.id == plan.candidate.entry)
114            .ok_or_else(|| RuntimeError::Plugin("entry definition disappeared".to_owned()))?
115            .clone();
116        let mut environment = BTreeMap::new();
117        let outcome = self.execute_region(
118            &run_id,
119            &plan,
120            &manifest,
121            &definition.body,
122            input,
123            ROOT_SCOPE_ID,
124            &definition.id,
125            &mut environment,
126        )?;
127
128        self.submit(
129            &run_id,
130            Command::CommitScope {
131                scope_id: ROOT_SCOPE_ID.to_owned(),
132            },
133        )?;
134        self.dispatch_pending(&run_id, &manifest, outcome.pending, &mut environment)?;
135        self.submit(
136            &run_id,
137            Command::YieldAttempt {
138                attempt_id: "attempt:root/1".to_owned(),
139                epoch: 0,
140            },
141        )?;
142
143        let result_bytes = canonical_bytes(&outcome.value)?;
144        let result_ref = self.machine.put_artifact("cymule.result/1", result_bytes);
145        self.submit(
146            &run_id,
147            Command::CompleteRun {
148                result: Some(result_ref),
149            },
150        )?;
151        self.machine.verify_replay()?;
152        let run = self
153            .machine
154            .projection()
155            .runs
156            .get(&run_id)
157            .ok_or_else(|| RuntimeError::Plugin("Run projection is missing".to_owned()))?;
158        Ok(ExecutionResult {
159            run_id,
160            plan_id: plan.plan_id,
161            value: outcome.value,
162            projection_digest: self.machine.projection().digest()?,
163            precondition_token: run.precondition_token(),
164            effects: run.effects.keys().cloned().collect(),
165        })
166    }
167
168    #[allow(clippy::too_many_arguments)]
169    fn execute_region(
170        &mut self,
171        run_id: &str,
172        plan: &SealedPlan,
173        manifest: &PluginManifest,
174        region: &Region,
175        input: &Value,
176        scope_id: &str,
177        invocation_id: &str,
178        environment: &mut BTreeMap<String, Value>,
179    ) -> RuntimeResult<RegionOutcome> {
180        let mut pending = Vec::new();
181        for step in &region.steps {
182            match &step.operation {
183                Operation::Call {
184                    component,
185                    input: expression,
186                    bind,
187                } => {
188                    let value = evaluate(expression, input, environment)?;
189                    let response = self.plugin.invoke(PluginRequest::Call {
190                        component: component.clone(),
191                        input: value,
192                    })?;
193                    let PluginResponse::CallResult { value } = response else {
194                        return Err(RuntimeError::Plugin(format!(
195                            "component {component} returned unexpected response {response:?}"
196                        )));
197                    };
198                    if let Some(binding) = bind {
199                        environment.insert(binding.clone(), value);
200                    }
201                }
202                Operation::Invoke {
203                    definition,
204                    input: expression,
205                    bind,
206                } => {
207                    let value = evaluate(expression, input, environment)?;
208                    let target = plan
209                        .candidate
210                        .definitions
211                        .iter()
212                        .find(|candidate| candidate.id == *definition)
213                        .expect("plan validation guarantees invoked definition");
214                    self.invocation_sequence += 1;
215                    let child_invocation =
216                        format!("{invocation_id}/{}:{}", step.id, self.invocation_sequence);
217                    let mut child_environment = BTreeMap::new();
218                    let child = self.execute_region(
219                        run_id,
220                        plan,
221                        manifest,
222                        &target.body,
223                        &value,
224                        scope_id,
225                        &child_invocation,
226                        &mut child_environment,
227                    )?;
228                    pending.extend(child.pending);
229                    if let Some(binding) = bind {
230                        environment.insert(binding.clone(), child.value);
231                    }
232                }
233                Operation::Wait { wait } => {
234                    return Err(RuntimeError::Suspended(format!("waiting for {wait:?}")));
235                }
236                Operation::Effect {
237                    effect,
238                    input: expression,
239                    occurrence,
240                    bind,
241                } => {
242                    let value = evaluate(expression, input, environment)?;
243                    let args = self
244                        .machine
245                        .put_artifact("cymule.effect-args/1", canonical_bytes(&value)?);
246                    let occurrence_binding = format!(
247                        "binding:{}/effect/{}/{}",
248                        manifest.implementation_id,
249                        effect,
250                        manifest
251                            .effects
252                            .get(effect)
253                            .expect("manifest was validated")
254                            .implementation_revision
255                    );
256                    let epoch = self.current_epoch(run_id)?;
257                    let intent_id = effect_intent_id(
258                        run_id,
259                        invocation_id,
260                        &step.id,
261                        scope_id,
262                        epoch,
263                        occurrence,
264                        &args,
265                        "cymule.effect-schema/1",
266                    )?;
267                    self.submit(
268                        run_id,
269                        Command::ProposeEffect {
270                            scope_id: scope_id.to_owned(),
271                            invocation_id: invocation_id.to_owned(),
272                            site_id: step.id.clone(),
273                            occurrence: occurrence.clone(),
274                            operation: effect.clone(),
275                            args,
276                            occurrence_binding,
277                        },
278                    )?;
279                    match self.plugin.invoke(PluginRequest::PrepareEffect {
280                        operation: effect.clone(),
281                        intent_id: intent_id.clone(),
282                        input: value.clone(),
283                    })? {
284                        PluginResponse::Prepared => {}
285                        response => {
286                            return Err(RuntimeError::Plugin(format!(
287                                "effect {effect} prepare returned {response:?}"
288                            )));
289                        }
290                    }
291                    self.submit(
292                        run_id,
293                        Command::TransitionEffect {
294                            intent_id: intent_id.clone(),
295                            transition: EffectTransition::Prepare,
296                        },
297                    )?;
298
299                    let contract = plan
300                        .candidate
301                        .effects
302                        .iter()
303                        .find(|contract| contract.id == *effect)
304                        .expect("plan validation guarantees effect contract");
305                    if contract.profile.mutation == MutationKind::Observational
306                        && contract.profile.dispatch == DispatchPolicy::Eager
307                    {
308                        let result = self.dispatch_effect(run_id, effect, intent_id, value)?;
309                        if let (Some(binding), Some(result)) = (bind, result) {
310                            environment.insert(binding.clone(), result);
311                        }
312                    } else {
313                        if bind.is_some() {
314                            return Err(RuntimeError::Plugin(format!(
315                                "deferred mutating effect {effect} cannot bind a value inside its open scope"
316                            )));
317                        }
318                        pending.push(PendingEffect {
319                            intent_id,
320                            operation: effect.clone(),
321                            input: value,
322                            bind: bind.clone(),
323                        });
324                    }
325                }
326                Operation::Scope { body, bind, .. } => {
327                    self.scope_sequence += 1;
328                    let child_scope = format!("scope:{}:{}", step.id, self.scope_sequence);
329                    self.submit(
330                        run_id,
331                        Command::OpenScope {
332                            scope_id: child_scope.clone(),
333                            parent_scope: scope_id.to_owned(),
334                        },
335                    )?;
336                    let mut child_environment = environment.clone();
337                    let child = self.execute_region(
338                        run_id,
339                        plan,
340                        manifest,
341                        body,
342                        input,
343                        &child_scope,
344                        invocation_id,
345                        &mut child_environment,
346                    )?;
347                    self.submit(
348                        run_id,
349                        Command::CommitScope {
350                            scope_id: child_scope,
351                        },
352                    )?;
353                    self.dispatch_pending(run_id, manifest, child.pending, &mut child_environment)?;
354                    if let Some(binding) = bind {
355                        environment.insert(binding.clone(), child.value);
356                    }
357                }
358            }
359        }
360        let value = evaluate(&region.result, input, environment)?;
361        Ok(RegionOutcome { value, pending })
362    }
363
364    fn dispatch_pending(
365        &mut self,
366        run_id: &str,
367        _manifest: &PluginManifest,
368        pending: Vec<PendingEffect>,
369        environment: &mut BTreeMap<String, Value>,
370    ) -> RuntimeResult<()> {
371        for effect in pending {
372            let result =
373                self.dispatch_effect(run_id, &effect.operation, effect.intent_id, effect.input)?;
374            if let (Some(binding), Some(result)) = (effect.bind, result) {
375                environment.insert(binding, result);
376            }
377        }
378        Ok(())
379    }
380
381    fn dispatch_effect(
382        &mut self,
383        run_id: &str,
384        operation: &str,
385        intent_id: String,
386        input: Value,
387    ) -> RuntimeResult<Option<Value>> {
388        self.submit(
389            run_id,
390            Command::TransitionEffect {
391                intent_id: intent_id.clone(),
392                transition: EffectTransition::AuthorizeRelease,
393            },
394        )?;
395        self.submit(
396            run_id,
397            Command::TransitionEffect {
398                intent_id: intent_id.clone(),
399                transition: EffectTransition::StartDispatch,
400            },
401        )?;
402        let response = self.plugin.invoke(PluginRequest::DispatchEffect {
403            operation: operation.to_owned(),
404            intent_id: intent_id.clone(),
405            input: input.clone(),
406        })?;
407        let PluginResponse::EffectResult { outcome, mut value } = response else {
408            return Err(RuntimeError::Plugin(format!(
409                "effect {operation} dispatch returned {response:?}"
410            )));
411        };
412        self.submit(
413            run_id,
414            Command::TransitionEffect {
415                intent_id: intent_id.clone(),
416                transition: EffectTransition::Observe(outcome),
417            },
418        )?;
419        if outcome == WorldOutcome::Unknown {
420            let response = self.plugin.invoke(PluginRequest::ReconcileEffect {
421                operation: operation.to_owned(),
422                intent_id: intent_id.clone(),
423                input,
424            })?;
425            let PluginResponse::ReconciliationResult {
426                resolution,
427                value: reconciled_value,
428            } = response
429            else {
430                return Err(RuntimeError::Plugin(format!(
431                    "effect {operation} reconciliation returned {response:?}"
432                )));
433            };
434            self.submit(
435                run_id,
436                Command::TransitionEffect {
437                    intent_id,
438                    transition: EffectTransition::Reconcile(resolution),
439                },
440            )?;
441            if matches!(
442                resolution,
443                ReconciliationResolution::ResolvedApplied
444                    | ReconciliationResolution::ResolvedNotApplied
445            ) {
446                value = reconciled_value.or(value);
447            }
448        }
449        Ok(value)
450    }
451
452    fn submit(&mut self, run_id: &str, command: Command) -> RuntimeResult<()> {
453        self.command_sequence += 1;
454        let expected_precondition = if matches!(command, Command::StartRun { .. }) {
455            None
456        } else {
457            Some(
458                self.machine
459                    .projection()
460                    .runs
461                    .get(run_id)
462                    .ok_or_else(|| RuntimeError::Plugin(format!("Run {run_id} is missing")))?
463                    .precondition_token(),
464            )
465        };
466        let receipt = self.machine.submit(CommandEnvelope {
467            command_version: COMMAND_VERSION.to_owned(),
468            command_id: format!("{run_id}:command:{}", self.command_sequence),
469            actor: "actor:embedded-runtime".to_owned(),
470            run_id: run_id.to_owned(),
471            expected_precondition,
472            command,
473        })?;
474        if receipt.status != CommandReceiptStatus::Applied {
475            return Err(RuntimeError::Plugin(format!(
476                "runtime command unexpectedly conflicted: {receipt:?}"
477            )));
478        }
479        Ok(())
480    }
481
482    fn current_epoch(&self, run_id: &str) -> RuntimeResult<u64> {
483        self.machine
484            .projection()
485            .runs
486            .get(run_id)
487            .map(|run| run.epoch)
488            .ok_or_else(|| RuntimeError::Plugin(format!("Run {run_id} is missing")))
489    }
490}
491
492fn validate_manifest(plan: &SealedPlan, manifest: &PluginManifest) -> RuntimeResult<()> {
493    for contract in &plan.candidate.components {
494        if !manifest.components.contains_key(&contract.id) {
495            return Err(RuntimeError::Plugin(format!(
496                "plugin does not implement component {}",
497                contract.id
498            )));
499        }
500    }
501    for contract in &plan.candidate.effects {
502        if !manifest.effects.contains_key(&contract.id) {
503            return Err(RuntimeError::Plugin(format!(
504                "plugin does not implement effect {}",
505                contract.id
506            )));
507        }
508    }
509    Ok(())
510}
511
512fn evaluate(
513    expression: &Expression,
514    input: &Value,
515    environment: &BTreeMap<String, Value>,
516) -> RuntimeResult<Value> {
517    match expression {
518        Expression::Input => Ok(input.clone()),
519        Expression::Literal { value } => Ok(value.clone()),
520        Expression::Binding { name } => environment.get(name).cloned().ok_or_else(|| {
521            RuntimeError::Plugin(format!("binding {name} is unavailable during execution"))
522        }),
523        Expression::Object { fields } => {
524            let mut object = serde_json::Map::new();
525            for (key, expression) in fields {
526                object.insert(key.clone(), evaluate(expression, input, environment)?);
527            }
528            Ok(Value::Object(object))
529        }
530        Expression::Array { items } => items
531            .iter()
532            .map(|expression| evaluate(expression, input, environment))
533            .collect::<RuntimeResult<Vec<_>>>()
534            .map(Value::Array),
535    }
536}