aranya_runtime/
vm_policy.rs

1//! VmPolicy implements a [Policy] that evaluates actions and commands via the [Policy
2//! VM](../../policy_vm/index.html).
3//!
4//! ## Creating a `VmPolicy` instance
5//!
6//! To use `VmPolicy` in your [`Engine`](super::Engine), you need to provide a Policy VM
7//! [`Machine`], a [`aranya_crypto::Engine`], and a Vec of Boxed FFI implementations. The Machine
8//! will be created by either compiling a policy document (see
9//! [`parse_policy_document()`](../../policy_lang/lang/fn.parse_policy_document.html) and
10//! [`Compiler`](../../policy_compiler/struct.Compiler.html)), or loading a compiled policy
11//! module (see [`Machine::from_module()`]). The crypto engine comes from your favorite
12//! implementation
13//! ([`DefaultEngine::from_entropy()`](aranya_crypto::default::DefaultEngine::from_entropy) is a
14//! good choice for testing). The list of FFIs is a list of things that implement
15//! [`FfiModule`](aranya_policy_vm::ffi::FfiModule), most likely via the [ffi attribute
16//! macro](../../policy_vm/ffi/attr.ffi.html). The list of FFI modules _must_ be in the same
17//! order as the FFI schemas given during VM construction.
18//!
19//! ```ignore
20//! // Create a `Machine` by compiling policy from source.
21//! let ast = parse_policy_document(policy_doc).unwrap();
22//! let machine = Compiler::new(&ast)
23//!     .ffi_modules(&[TestFfiEnvelope::SCHEMA])
24//!     .compile()
25//!     .unwrap();
26//! // Create a `aranya_crypto::Engine` implementation
27//! let (eng, _) = DefaultEngine::from_entropy(Rng);
28//! // Create a list of FFI module implementations
29//! let ffi_modules = vec![Box::from(TestFfiEnvelope {
30//!     device: DeviceId::random(&mut Rng),
31//! })];
32//! // And finally, create the VmPolicy
33//! let policy = VmPolicy::new(machine, eng, ffi_modules).unwrap();
34//! ```
35//!
36//! ## Actions and Effects
37//!
38//! The VM represents actions as a kind of function, which has a name and a list of
39//! parameters. [`VmPolicy`] represents those actions as [`VmAction`]. Calling an action
40//! via [`call_action()`](VmPolicy::call_action) requires you to give it an action of
41//! that type. You can use the [`vm_action!()`](crate::vm_action) macro to create this
42//! more comfortably.
43//!
44//! The VM represents effects as a named struct containing a set of fields. `VmPolicy`
45//! represents this as [`VmEffect`]. Effects captured via [`Sink`]s will have this type.
46//! You can use the [`vm_effect!()`](crate::vm_effect) macro to create effects.
47//!
48//! ## The "init" command and action
49//!
50//! To create a graph, there must be a command that is the ancestor of all commands in that
51//! graph - the "init" command. In `VmPolicy`, that command is created via a special action
52//! given as the second argument to
53//! [`ClientState::new_graph()`](crate::ClientState::new_graph). The first command produced
54//! by that action becomes the "init" command. It has basically all the same properties as
55//! any other command, except it has no parent.
56//!
57//! So for this example policy:
58//!
59//! ```policy
60//! command Init {
61//!     fields {
62//!         nonce int,
63//!     }
64//!     seal { ... }
65//!     open { ... }
66//!     policy {
67//!         finish {}
68//!     }
69//! }
70//!
71//! action init(nonce int) {
72//!     publish Init {
73//!         nonce: nonce,
74//!     }
75//! }
76//! ```
77//!
78//! This is an example of initializing a graph with `new_graph()`:
79//!
80//! ```ignore
81//! let engine = MyEngine::new();
82//! let provider = MyStorageProvider::new();
83//! let mut cs = ClientState::new(engine, provider);
84//! let mut sink = MySink::new();
85//!
86//! let storage_id = cs
87//!     .new_graph(&[0u8], vm_action!(init(0)), &mut sink)
88//!     .expect("could not create graph");
89//! ```
90//!
91//! Because the ID of this initial command is also the storage ID of the resulting graph,
92//! some data within the command must be present to ensure that multiple initial commands
93//! create distinct IDs for each graph. If no other suitable data exists, it is good
94//! practice to add a nonce field that is distinct for each graph.
95//!
96//! ## Priorities
97//!
98//! `VmPolicy` uses the policy language's attributes system to report command priorities to
99//! the runtime. You can specify the priority of a command by adding the `priority`
100//! attribute. It should be an `int` literal.
101//!
102//! ```policy
103//! command Foo {
104//!     attributes {
105//!         priority: 3
106//!     }
107//!     // ... fields, policy, etc.
108//! }
109//! ```
110//!
111//! ## Policy Interface Generator
112//!
113//! A more comfortable way to use `VmPolicy` is via the [Policy Interface
114//! Generator](../../policy_ifgen/index.html). It creates a Rust interface for actions and
115//! effects from a policy document.
116
117extern crate alloc;
118
119use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, rc::Rc, string::String, vec::Vec};
120use core::{borrow::Borrow, cell::RefCell, fmt};
121
122use aranya_policy_vm::{
123    ActionContext, CommandContext, ExitReason, KVPair, Machine, MachineIO, MachineStack,
124    OpenContext, PolicyContext, RunState, Stack, Struct, Value,
125    ast::{Identifier, Persistence},
126};
127use buggy::{BugExt, bug};
128use spin::Mutex;
129use tracing::{error, info, instrument};
130
131use crate::{
132    ActionPlacement, CommandPlacement, FactPerspective, MergeIds, Perspective, Prior, Priority,
133    command::{CmdId, Command},
134    engine::{EngineError, NullSink, Policy, Sink},
135};
136
137mod error;
138mod io;
139mod protocol;
140pub mod testing;
141
142pub use error::*;
143pub use io::*;
144pub use protocol::*;
145
146/// Creates a [`VmAction`].
147///
148/// This must be used directly to avoid lifetime issues, not assigned to a variable.
149///
150/// # Example
151///
152/// ```ignore
153/// let x = 42;
154/// let y = text!("asdf");
155/// client.action(storage_id, sink, vm_action!(foobar(x, y)))
156/// ```
157#[macro_export]
158macro_rules! vm_action {
159    ($name:ident($($arg:expr),* $(,)?)) => {
160        $crate::VmAction {
161            name: ::aranya_policy_vm::ident!(stringify!($name)),
162            args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
163        }
164    };
165}
166
167/// Creates a [`VmEffectData`].
168///
169/// This is mostly useful for testing expected effects, and is expected to be compared
170/// against a [`VmEffect`].
171///
172/// # Example
173///
174/// ```ignore
175/// let val = 3;
176/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
177///
178/// client.action(storage_id, sink, vm_action!(create(val)))
179/// ```
180#[macro_export]
181macro_rules! vm_effect {
182    ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
183        $crate::VmEffectData {
184            name: ::aranya_policy_vm::ident!(stringify!($name)),
185            fields: vec![$(
186                ::aranya_policy_vm::KVPair::new(::aranya_policy_vm::ident!(stringify!($field)), $val.into())
187            ),*],
188        }
189    };
190}
191
192/// A [Policy] implementation that uses the Policy VM.
193pub struct VmPolicy<E> {
194    machine: Machine,
195    engine: Mutex<E>,
196    ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
197    priority_map: BTreeMap<Identifier, VmPriority>,
198}
199
200impl<E> VmPolicy<E> {
201    /// Create a new `VmPolicy` from a [Machine]
202    pub fn new(
203        machine: Machine,
204        engine: E,
205        ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
206    ) -> Result<Self, VmPolicyError> {
207        let priority_map = get_command_priorities(&machine)?;
208        Ok(Self {
209            machine,
210            engine: Mutex::new(engine),
211            ffis,
212            priority_map,
213        })
214    }
215
216    fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
217    where
218        M: MachineIO<MachineStack>,
219    {
220        rs.source_location()
221            .unwrap_or_else(|| String::from("(unknown location)"))
222    }
223}
224
225/// Scans command attributes for priorities and creates the priority map from them.
226fn get_command_priorities(
227    machine: &Machine,
228) -> Result<BTreeMap<Identifier, VmPriority>, AttributeError> {
229    let mut priority_map = BTreeMap::new();
230    for def in machine.command_defs.iter() {
231        let name = &def.name;
232        let attrs = &def.attributes;
233        let finalize = attrs
234            .get("finalize")
235            .map(|attr| match attr.value {
236                Value::Bool(b) => Ok(b),
237                _ => Err(AttributeError::type_mismatch(
238                    name.as_str(),
239                    "finalize",
240                    "Bool",
241                    &attr.value.type_name(),
242                )),
243            })
244            .transpose()?
245            == Some(true);
246        let priority: Option<u32> = attrs
247            .get("priority")
248            .map(|attr| match attr.value {
249                Value::Int(b) => b.try_into().map_err(|_| {
250                    AttributeError::int_range(
251                        name.as_str(),
252                        "priority",
253                        u32::MIN.into(),
254                        u32::MAX.into(),
255                    )
256                }),
257                _ => Err(AttributeError::type_mismatch(
258                    name.as_str(),
259                    "priority",
260                    "Int",
261                    &attr.value.type_name(),
262                )),
263            })
264            .transpose()?;
265        match (finalize, priority) {
266            (false, None) => {}
267            (false, Some(p)) => {
268                priority_map.insert(name.name.clone(), VmPriority::Basic(p));
269            }
270            (true, None) => {
271                priority_map.insert(name.name.clone(), VmPriority::Finalize);
272            }
273            (true, Some(_)) => {
274                return Err(AttributeError::exclusive(
275                    name.as_str(),
276                    "finalize",
277                    "priority",
278                ));
279            }
280        }
281    }
282    Ok(priority_map)
283}
284
285impl<E: aranya_crypto::Engine> VmPolicy<E> {
286    #[allow(clippy::too_many_arguments)]
287    #[instrument(skip_all, fields(name = name.as_str()))]
288    fn evaluate_rule<'a, P>(
289        &self,
290        name: Identifier,
291        fields: &[KVPair],
292        envelope: Envelope<'_>,
293        facts: &'a mut P,
294        sink: &'a mut impl Sink<VmEffect>,
295        ctx: CommandContext,
296        placement: CommandPlacement,
297    ) -> Result<(), EngineError>
298    where
299        P: FactPerspective,
300    {
301        let facts = RefCell::new(facts);
302        let sink = RefCell::new(sink);
303        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
304        let mut rs = self.machine.create_run_state(&io, ctx);
305        let this_data = Struct::new(name, fields);
306        match rs.call_command_policy(this_data.clone(), envelope.clone().into()) {
307            Ok(reason) => match reason {
308                ExitReason::Normal => Ok(()),
309                ExitReason::Yield => bug!("unexpected yield"),
310                ExitReason::Check => {
311                    info!("Check {}", self.source_location(&rs));
312
313                    match placement {
314                        CommandPlacement::OnGraphAtOrigin | CommandPlacement::OffGraph => {
315                            // Immediate check failure.
316                            return Err(EngineError::Check);
317                        }
318                        CommandPlacement::OnGraphInBraid => {
319                            // Perform recall.
320                        }
321                    }
322
323                    // Construct a new recall context from the policy context
324                    let CommandContext::Policy(policy_ctx) = rs.get_context() else {
325                        error!(
326                            "Non-policy context while evaluating rule: {:?}",
327                            rs.get_context()
328                        );
329                        return Err(EngineError::InternalError);
330                    };
331                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
332                    rs.set_context(recall_ctx);
333                    self.recall_internal(&mut rs, this_data, envelope)
334                }
335                ExitReason::Panic => {
336                    info!("Panicked {}", self.source_location(&rs));
337                    Err(EngineError::Panic)
338                }
339            },
340            Err(e) => {
341                error!("\n{e}");
342                Err(EngineError::InternalError)
343            }
344        }
345    }
346
347    fn recall_internal<M>(
348        &self,
349        rs: &mut RunState<'_, M>,
350        this_data: Struct,
351        envelope: Envelope<'_>,
352    ) -> Result<(), EngineError>
353    where
354        M: MachineIO<MachineStack>,
355    {
356        match rs.call_command_recall(this_data, envelope.into()) {
357            Ok(ExitReason::Normal) => Err(EngineError::Check),
358            Ok(ExitReason::Yield) => bug!("unexpected yield"),
359            Ok(ExitReason::Check) => {
360                info!("Recall failed: {}", self.source_location(rs));
361                Err(EngineError::Check)
362            }
363            Ok(ExitReason::Panic) | Err(_) => {
364                info!("Recall panicked: {}", self.source_location(rs));
365                Err(EngineError::Panic)
366            }
367        }
368    }
369
370    #[instrument(skip_all, fields(name = name.as_str()))]
371    fn open_command<P>(
372        &self,
373        name: Identifier,
374        envelope: Envelope<'_>,
375        facts: &mut P,
376    ) -> Result<Struct, EngineError>
377    where
378        P: FactPerspective,
379    {
380        let facts = RefCell::new(facts);
381        let mut sink = NullSink;
382        let sink2 = RefCell::new(&mut sink);
383        let io = RefCell::new(VmPolicyIO::new(&facts, &sink2, &self.engine, &self.ffis));
384        let ctx = CommandContext::Open(OpenContext { name: name.clone() });
385        let mut rs = self.machine.create_run_state(&io, ctx);
386        let status = rs.call_open(name, envelope.into());
387        match status {
388            Ok(reason) => match reason {
389                ExitReason::Normal => {
390                    let v = rs.consume_return().map_err(|e| {
391                        error!("Could not pull envelope from stack: {e}");
392                        EngineError::InternalError
393                    })?;
394                    Ok(v.try_into().map_err(|e| {
395                        error!("Envelope is not a struct: {e}");
396                        EngineError::InternalError
397                    })?)
398                }
399                ExitReason::Yield => bug!("unexpected yield"),
400                ExitReason::Check => {
401                    info!("Check {}", self.source_location(&rs));
402                    Err(EngineError::Check)
403                }
404                ExitReason::Panic => {
405                    info!("Panicked {}", self.source_location(&rs));
406                    Err(EngineError::Check)
407                }
408            },
409            Err(e) => {
410                error!("\n{e}");
411                Err(EngineError::InternalError)
412            }
413        }
414    }
415}
416
417/// [`VmPolicy`]'s actions.
418#[derive(Clone, Debug, PartialEq, Eq)]
419pub struct VmAction<'a> {
420    /// The name of the action.
421    pub name: Identifier,
422    /// The arguments of the action.
423    pub args: Cow<'a, [Value]>,
424}
425
426/// A partial version of [`VmEffect`] containing only the data. Created by
427/// [`vm_effect!`] and used to compare only the name and fields against the full
428/// `VmEffect`.
429#[derive(Debug)]
430pub struct VmEffectData {
431    /// The name of the effect.
432    pub name: Identifier,
433    /// The fields of the effect.
434    pub fields: Vec<KVPair>,
435}
436
437impl PartialEq<VmEffect> for VmEffectData {
438    fn eq(&self, other: &VmEffect) -> bool {
439        self.name == other.name && self.fields == other.fields
440    }
441}
442
443impl PartialEq<VmEffectData> for VmEffect {
444    fn eq(&self, other: &VmEffectData) -> bool {
445        self.name == other.name && self.fields == other.fields
446    }
447}
448
449/// [`VmPolicy`]'s effects.
450#[derive(Clone, Debug, PartialEq, Eq)]
451pub struct VmEffect {
452    /// The name of the effect.
453    pub name: Identifier,
454    /// The fields of the effect.
455    pub fields: Vec<KVPair>,
456    /// The command ID that produced this effect
457    pub command: CmdId,
458    /// Was this produced from a recall block?
459    pub recalled: bool,
460}
461
462#[derive(Copy, Clone, Debug, PartialEq)]
463enum VmPriority {
464    Basic(u32),
465    Finalize,
466}
467
468impl Default for VmPriority {
469    fn default() -> Self {
470        Self::Basic(0)
471    }
472}
473
474impl From<VmPriority> for Priority {
475    fn from(value: VmPriority) -> Self {
476        match value {
477            VmPriority::Basic(p) => Self::Basic(p),
478            VmPriority::Finalize => Self::Finalize,
479        }
480    }
481}
482
483impl<E> VmPolicy<E> {
484    fn get_command_priority(&self, name: &Identifier) -> VmPriority {
485        debug_assert!(self.machine.command_defs.contains(name));
486        self.priority_map.get(name).copied().unwrap_or_default()
487    }
488}
489
490impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
491    type Action<'a> = VmAction<'a>;
492    type Effect = VmEffect;
493    type Command<'a> = VmProtocol<'a>;
494
495    fn serial(&self) -> u32 {
496        // TODO(chip): Implement an actual serial number
497        0u32
498    }
499
500    #[instrument(skip_all)]
501    fn call_rule(
502        &self,
503        command: &impl Command,
504        facts: &mut impl FactPerspective,
505        sink: &mut impl Sink<Self::Effect>,
506        placement: CommandPlacement,
507    ) -> Result<(), EngineError> {
508        let unpacked: VmProtocolData<'_> = postcard::from_bytes(command.bytes()).map_err(|e| {
509            error!("Could not deserialize: {e:?}");
510            EngineError::Read
511        })?;
512        let (command_info, expected_priority) = {
513            match unpacked {
514                VmProtocolData::Init {
515                    author_id,
516                    kind,
517                    serialized_fields,
518                    signature,
519                    ..
520                } => (
521                    Some((
522                        Envelope {
523                            parent_id: CmdId::default(),
524                            author_id,
525                            command_id: command.id(),
526                            payload: Cow::Borrowed(serialized_fields),
527                            signature: Cow::Borrowed(signature),
528                        },
529                        kind,
530                        author_id,
531                    )),
532                    Priority::Init,
533                ),
534                VmProtocolData::Basic {
535                    parent,
536                    kind,
537                    author_id,
538                    serialized_fields,
539                    signature,
540                } => {
541                    let priority = self.get_command_priority(&kind).into();
542                    (
543                        Some((
544                            Envelope {
545                                parent_id: parent.id,
546                                author_id,
547                                command_id: command.id(),
548                                payload: Cow::Borrowed(serialized_fields),
549                                signature: Cow::Borrowed(signature),
550                            },
551                            kind,
552                            author_id,
553                        )),
554                        priority,
555                    )
556                }
557                // Merges always pass because they're an artifact of the graph
558                _ => (None, Priority::Merge),
559            }
560        };
561
562        if command.priority() != expected_priority {
563            error!(
564                "Expected priority {:?}, got {:?}",
565                expected_priority,
566                command.priority()
567            );
568            bug!("Command has invalid priority");
569        }
570
571        if let Some((envelope, kind, author_id)) = command_info {
572            let def = self.machine.command_defs.get(&kind).ok_or_else(|| {
573                error!("unknown command {kind}");
574                EngineError::InternalError
575            })?;
576
577            match (placement, &def.persistence) {
578                (CommandPlacement::OnGraphAtOrigin, Persistence::Persistent) => {}
579                (CommandPlacement::OnGraphInBraid, Persistence::Persistent) => {}
580                (CommandPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
581                (CommandPlacement::OnGraphAtOrigin, Persistence::Ephemeral(_)) => {
582                    error!("cannot evaluate ephemeral command on-graph");
583                    return Err(EngineError::InternalError);
584                }
585                (CommandPlacement::OnGraphInBraid, Persistence::Ephemeral(_)) => {
586                    error!("cannot evaluate ephemeral command in braid");
587                    return Err(EngineError::InternalError);
588                }
589                (CommandPlacement::OffGraph, Persistence::Persistent) => {
590                    error!("cannot evaluate persistent command off-graph");
591                    return Err(EngineError::InternalError);
592                }
593            }
594
595            let command_struct = self.open_command(kind.clone(), envelope.clone(), facts)?;
596            let fields: Vec<KVPair> = command_struct
597                .fields
598                .into_iter()
599                .map(|(k, v)| KVPair::new(k, v))
600                .collect();
601            let ctx = CommandContext::Policy(PolicyContext {
602                name: kind.clone(),
603                id: command.id(),
604                author: author_id,
605                version: CmdId::default().into(),
606            });
607            self.evaluate_rule(
608                kind,
609                fields.as_slice(),
610                envelope,
611                facts,
612                sink,
613                ctx,
614                placement,
615            )?
616        }
617        Ok(())
618    }
619
620    #[instrument(skip_all, fields(name = action.name.as_str()))]
621    fn call_action(
622        &self,
623        action: Self::Action<'_>,
624        facts: &mut impl Perspective,
625        sink: &mut impl Sink<Self::Effect>,
626        action_placement: ActionPlacement,
627    ) -> Result<(), EngineError> {
628        let VmAction { name, args } = action;
629
630        let def = self
631            .machine
632            .action_defs
633            .get(&name)
634            .ok_or(EngineError::InternalError)?;
635
636        match (action_placement, &def.persistence) {
637            (ActionPlacement::OnGraph, Persistence::Persistent) => {}
638            (ActionPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
639            (ActionPlacement::OnGraph, Persistence::Ephemeral(_)) => {
640                error!("cannot call ephemeral action on-graph");
641                return Err(EngineError::InternalError);
642            }
643            (ActionPlacement::OffGraph, Persistence::Persistent) => {
644                error!("cannot call persistent action off-graph");
645                return Err(EngineError::InternalError);
646            }
647        }
648
649        let parent = match facts.head_address()? {
650            Prior::None => None,
651            Prior::Single(id) => Some(id),
652            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
653        };
654        // FIXME(chip): This is kind of wrong, but it avoids having to
655        // plumb Option<Id> into the VM and FFI
656        let ctx_parent = parent.unwrap_or_default();
657        let facts = Rc::new(RefCell::new(facts));
658        let sink = Rc::new(RefCell::new(sink));
659        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
660        let ctx = CommandContext::Action(ActionContext {
661            name: name.clone(),
662            head_id: ctx_parent.id,
663        });
664        let command_placement = match action_placement {
665            ActionPlacement::OnGraph => CommandPlacement::OnGraphAtOrigin,
666            ActionPlacement::OffGraph => CommandPlacement::OffGraph,
667        };
668        {
669            let mut rs = self.machine.create_run_state(&io, ctx);
670            let mut exit_reason = match args {
671                Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
672                Cow::Owned(args) => rs.call_action(name, args),
673            }
674            .map_err(|e| {
675                error!("\n{e}");
676                EngineError::InternalError
677            })?;
678            loop {
679                match exit_reason {
680                    ExitReason::Normal => {
681                        // Action completed
682                        break;
683                    }
684                    ExitReason::Yield => {
685                        // Command was published.
686                        let command_struct: Struct = rs.stack.pop().map_err(|e| {
687                            error!("should have command struct: {e}");
688                            EngineError::InternalError
689                        })?;
690                        let command_name = command_struct.name.clone();
691
692                        let seal_ctx = rs.get_context().seal_from_action(command_name.clone())?;
693                        let mut rs_seal = self.machine.create_run_state(&io, seal_ctx);
694                        match rs_seal.call_seal(command_struct).map_err(|e| {
695                            error!("Cannot seal command: {}", e);
696                            EngineError::Panic
697                        })? {
698                            ExitReason::Normal => (),
699                            r @ ExitReason::Yield
700                            | r @ ExitReason::Check
701                            | r @ ExitReason::Panic => {
702                                error!("Could not seal command: {}", r);
703                                return Err(EngineError::Panic);
704                            }
705                        }
706
707                        // Grab sealed envelope from stack
708                        let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
709                            error!("Expected a sealed envelope {e}");
710                            EngineError::InternalError
711                        })?;
712                        let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
713                            error!("Malformed envelope: {e}");
714                            EngineError::InternalError
715                        })?;
716
717                        // The parent of a basic command should be the command that was added to the perspective on the previous
718                        // iteration of the loop
719                        let parent = match RefCell::borrow_mut(Rc::borrow(&facts)).head_address()? {
720                            Prior::None => None,
721                            Prior::Single(id) => Some(id),
722                            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
723                        };
724
725                        let priority = if parent.is_some() {
726                            self.get_command_priority(&command_name).into()
727                        } else {
728                            Priority::Init
729                        };
730
731                        let data = match parent {
732                            None => VmProtocolData::Init {
733                                // TODO(chip): where does the policy value come from?
734                                policy: 0u64.to_le_bytes(),
735                                author_id: envelope.author_id,
736                                kind: command_name.clone(),
737                                serialized_fields: &envelope.payload,
738                                signature: &envelope.signature,
739                            },
740                            Some(parent) => VmProtocolData::Basic {
741                                author_id: envelope.author_id,
742                                parent,
743                                kind: command_name.clone(),
744                                serialized_fields: &envelope.payload,
745                                signature: &envelope.signature,
746                            },
747                        };
748                        let wrapped = postcard::to_allocvec(&data)
749                            .assume("can serialize vm protocol data")?;
750                        let new_command =
751                            VmProtocol::new(&wrapped, envelope.command_id, data, priority);
752
753                        self.call_rule(
754                            &new_command,
755                            *RefCell::borrow_mut(Rc::borrow(&facts)),
756                            *RefCell::borrow_mut(Rc::borrow(&sink)),
757                            command_placement,
758                        )?;
759                        RefCell::borrow_mut(Rc::borrow(&facts))
760                            .add_command(&new_command)
761                            .map_err(|e| {
762                                error!("{e}");
763                                EngineError::Write
764                            })?;
765
766                        // After publishing a new command, the RunState's context must be updated to reflect the new head
767                        rs.update_context_with_new_head(new_command.id())?;
768
769                        // Resume action after last Publish
770                        exit_reason = rs.run().map_err(|e| {
771                            error!("{e}");
772                            EngineError::InternalError
773                        })?;
774                    }
775                    ExitReason::Check => {
776                        info!("Check {}", self.source_location(&rs));
777                        return Err(EngineError::Check);
778                    }
779                    ExitReason::Panic => {
780                        info!("Panicked {}", self.source_location(&rs));
781                        return Err(EngineError::Panic);
782                    }
783                };
784            }
785        }
786
787        Ok(())
788    }
789
790    fn merge<'a>(
791        &self,
792        target: &'a mut [u8],
793        ids: MergeIds,
794    ) -> Result<Self::Command<'a>, EngineError> {
795        let (left, right) = ids.into();
796        let c = VmProtocolData::Merge { left, right };
797        let id = aranya_crypto::merge_cmd_id::<E::CS>(left.id, right.id);
798        let data = postcard::to_slice(&c, target).map_err(|e| {
799            error!("{e}");
800            EngineError::Write
801        })?;
802        Ok(VmProtocol::new(data, id, c, Priority::Merge))
803    }
804}
805
806impl fmt::Display for VmAction<'_> {
807    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808        let mut d = f.debug_tuple(self.name.as_str());
809        for arg in self.args.as_ref() {
810            d.field(&DebugViaDisplay(arg));
811        }
812        d.finish()
813    }
814}
815
816impl fmt::Display for VmEffect {
817    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
818        let mut d = f.debug_struct(self.name.as_str());
819        for field in &self.fields {
820            d.field(field.key().as_str(), &DebugViaDisplay(field.value()));
821        }
822        d.finish()
823    }
824}
825
826/// Implements `Debug` via `T`'s `Display` impl.
827struct DebugViaDisplay<T>(T);
828
829impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
830    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
831        write!(f, "{}", self.0)
832    }
833}
834
835#[cfg(test)]
836mod test {
837    use alloc::format;
838
839    use aranya_policy_compiler::Compiler;
840    use aranya_policy_lang::lang::parse_policy_str;
841    use aranya_policy_vm::ast::Version;
842
843    use super::*;
844
845    #[test]
846    fn test_get_command_priorities() {
847        fn process(attrs: &str) -> Result<Option<VmPriority>, AttributeError> {
848            let policy = format!(
849                r#"
850                command Test {{
851                    attributes {{
852                        {attrs}
853                    }}
854                    fields {{ }}
855                    seal {{ return todo() }}
856                    open {{ return todo() }}
857                    policy {{ }}
858                }}
859                "#
860            );
861            let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
862            let module = Compiler::new(&ast)
863                .compile()
864                .unwrap_or_else(|e| panic!("{e}"));
865            let machine = Machine::from_module(module).expect("can create machine");
866            let priorities = get_command_priorities(&machine)?;
867            Ok(priorities.get("Test").copied())
868        }
869
870        assert_eq!(process(""), Ok(None));
871        assert_eq!(process("finalize: false"), Ok(None));
872
873        assert_eq!(process("priority: 42"), Ok(Some(VmPriority::Basic(42))));
874        assert_eq!(
875            process("finalize: false, priority: 42"),
876            Ok(Some(VmPriority::Basic(42)))
877        );
878        assert_eq!(
879            process("priority: 42, finalize: false"),
880            Ok(Some(VmPriority::Basic(42)))
881        );
882
883        assert_eq!(process("finalize: true"), Ok(Some(VmPriority::Finalize)));
884
885        assert_eq!(
886            process("finalize: 42"),
887            Err(AttributeError::type_mismatch(
888                "Test", "finalize", "Bool", "Int"
889            ))
890        );
891        assert_eq!(
892            process("priority: false"),
893            Err(AttributeError::type_mismatch(
894                "Test", "priority", "Int", "Bool"
895            ))
896        );
897        assert_eq!(
898            process("priority: -1"),
899            Err(AttributeError::int_range(
900                "Test",
901                "priority",
902                u32::MIN.into(),
903                u32::MAX.into(),
904            ))
905        );
906        assert_eq!(
907            process(&format!("priority: {}", i64::MAX)),
908            Err(AttributeError::int_range(
909                "Test",
910                "priority",
911                u32::MIN.into(),
912                u32::MAX.into(),
913            ))
914        );
915        assert_eq!(
916            process("finalize: true, priority: 42"),
917            Err(AttributeError::exclusive("Test", "finalize", "priority"))
918        )
919    }
920}