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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ExecutionResult {
19 pub run_id: String,
21 pub plan_id: String,
23 pub value: Value,
25 pub projection_digest: String,
27 pub precondition_token: String,
29 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
47pub 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 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 pub const fn machine(&self) -> &Machine {
70 &self.machine
71 }
72
73 pub fn seal(&mut self, candidate: PlanCandidate) -> RuntimeResult<SealedPlan> {
75 self.machine.seal_plan(candidate).map_err(Into::into)
76 }
77
78 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 ®ion.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(®ion.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}