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};
126use buggy::{bug, BugExt};
127use spin::Mutex;
128use tracing::{error, info, instrument};
129
130use crate::{
131    command::{Command, CommandId},
132    engine::{EngineError, NullSink, Policy, Sink},
133    CommandRecall, FactPerspective, MergeIds, Perspective, Prior, Priority,
134};
135
136mod error;
137mod io;
138mod protocol;
139pub mod testing;
140
141pub use error::*;
142pub use io::*;
143pub use protocol::*;
144
145/// Creates a [`VmAction`].
146///
147/// This must be used directly to avoid lifetime issues, not assigned to a variable.
148///
149/// # Example
150///
151/// ```ignore
152/// let x = 42;
153/// let y = String::from("asdf");
154/// client.action(storage_id, sink, vm_action!(foobar(x, y)))
155/// ```
156#[macro_export]
157macro_rules! vm_action {
158    ($name:ident($($arg:expr),* $(,)?)) => {
159        $crate::VmAction {
160            name: stringify!($name),
161            args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
162        }
163    };
164}
165
166/// Creates a [`VmEffectData`].
167///
168/// This is mostly useful for testing expected effects, and is expected to be compared
169/// against a [`VmEffect`].
170///
171/// # Example
172///
173/// ```ignore
174/// let val = 3;
175/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
176///
177/// client.action(storage_id, sink, vm_action!(create(val)))
178/// ```
179#[macro_export]
180macro_rules! vm_effect {
181    ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
182        $crate::VmEffectData {
183            name: stringify!($name).into(),
184            fields: vec![$(
185                ::aranya_policy_vm::KVPair::new(stringify!($field), $val.into())
186            ),*],
187        }
188    };
189}
190
191/// A [Policy] implementation that uses the Policy VM.
192pub struct VmPolicy<E> {
193    machine: Machine,
194    engine: Mutex<E>,
195    ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
196    priority_map: BTreeMap<String, VmPriority>,
197}
198
199impl<E> VmPolicy<E> {
200    /// Create a new `VmPolicy` from a [Machine]
201    pub fn new(
202        machine: Machine,
203        engine: E,
204        ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
205    ) -> Result<Self, VmPolicyError> {
206        let priority_map = get_command_priorities(&machine)?;
207        Ok(Self {
208            machine,
209            engine: Mutex::new(engine),
210            ffis,
211            priority_map,
212        })
213    }
214
215    fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
216    where
217        M: MachineIO<MachineStack>,
218    {
219        rs.source_location()
220            .unwrap_or_else(|| String::from("(unknown location)"))
221    }
222}
223
224/// Scans command attributes for priorities and creates the priority map from them.
225fn get_command_priorities(
226    machine: &Machine,
227) -> Result<BTreeMap<String, VmPriority>, AttributeError> {
228    let mut priority_map = BTreeMap::new();
229    for (name, attrs) in &machine.command_attributes {
230        let finalize = attrs
231            .get("finalize")
232            .map(|attr| match *attr {
233                Value::Bool(b) => Ok(b),
234                _ => Err(AttributeError::type_mismatch(
235                    name,
236                    "finalize",
237                    "Bool",
238                    &attr.type_name(),
239                )),
240            })
241            .transpose()?
242            == Some(true);
243        let priority: Option<u32> = attrs
244            .get("priority")
245            .map(|attr| match *attr {
246                Value::Int(b) => b.try_into().map_err(|_| {
247                    AttributeError::int_range(name, "priority", u32::MIN.into(), u32::MAX.into())
248                }),
249                _ => Err(AttributeError::type_mismatch(
250                    name,
251                    "priority",
252                    "Int",
253                    &attr.type_name(),
254                )),
255            })
256            .transpose()?;
257        match (finalize, priority) {
258            (false, None) => {}
259            (false, Some(p)) => {
260                priority_map.insert(name.clone(), VmPriority::Basic(p));
261            }
262            (true, None) => {
263                priority_map.insert(name.clone(), VmPriority::Finalize);
264            }
265            (true, Some(_)) => {
266                return Err(AttributeError::exclusive(name, "finalize", "priority"));
267            }
268        }
269    }
270    Ok(priority_map)
271}
272
273impl<E: aranya_crypto::Engine> VmPolicy<E> {
274    #[allow(clippy::too_many_arguments)]
275    #[instrument(skip_all, fields(name = name))]
276    fn evaluate_rule<'a, P>(
277        &self,
278        name: &str,
279        fields: &[KVPair],
280        envelope: Envelope<'_>,
281        facts: &'a mut P,
282        sink: &'a mut impl Sink<VmEffect>,
283        ctx: CommandContext<'_>,
284        recall: CommandRecall,
285    ) -> Result<(), EngineError>
286    where
287        P: FactPerspective,
288    {
289        let facts = RefCell::new(facts);
290        let sink = RefCell::new(sink);
291        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
292        let mut rs = self.machine.create_run_state(&io, ctx);
293        let self_data = Struct::new(name, fields);
294        match rs.call_command_policy(&self_data.name, &self_data, envelope.clone().into()) {
295            Ok(reason) => match reason {
296                ExitReason::Normal => Ok(()),
297                ExitReason::Yield => bug!("unexpected yield"),
298                ExitReason::Check => {
299                    info!("Check {}", self.source_location(&rs));
300                    // Construct a new recall context from the policy context
301                    let CommandContext::Policy(policy_ctx) = rs.get_context() else {
302                        error!(
303                            "Non-policy context while evaluating rule: {:?}",
304                            rs.get_context()
305                        );
306                        return Err(EngineError::InternalError);
307                    };
308                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
309                    rs.set_context(recall_ctx);
310                    self.recall_internal(recall, &mut rs, name, &self_data, envelope)
311                }
312                ExitReason::Panic => {
313                    info!("Panicked {}", self.source_location(&rs));
314                    Err(EngineError::Panic)
315                }
316            },
317            Err(e) => {
318                error!("\n{e}");
319                Err(EngineError::InternalError)
320            }
321        }
322    }
323
324    fn recall_internal<M>(
325        &self,
326        recall: CommandRecall,
327        rs: &mut RunState<'_, M>,
328        name: &str,
329        self_data: &Struct,
330        envelope: Envelope<'_>,
331    ) -> Result<(), EngineError>
332    where
333        M: MachineIO<MachineStack>,
334    {
335        match recall {
336            CommandRecall::None => Err(EngineError::Check),
337            CommandRecall::OnCheck => {
338                match rs.call_command_recall(name, self_data, envelope.into()) {
339                    Ok(ExitReason::Normal) => Err(EngineError::Check),
340                    Ok(ExitReason::Yield) => bug!("unexpected yield"),
341                    Ok(ExitReason::Check) => {
342                        info!("Recall failed: {}", self.source_location(rs));
343                        Err(EngineError::Check)
344                    }
345                    Ok(ExitReason::Panic) | Err(_) => {
346                        info!("Recall panicked: {}", self.source_location(rs));
347                        Err(EngineError::Panic)
348                    }
349                }
350            }
351        }
352    }
353
354    #[instrument(skip_all, fields(name = name))]
355    fn open_command<P>(
356        &self,
357        name: &str,
358        envelope: Envelope<'_>,
359        facts: &mut P,
360    ) -> Result<Struct, EngineError>
361    where
362        P: FactPerspective,
363    {
364        let facts = RefCell::new(facts);
365        let mut sink = NullSink;
366        let sink2 = RefCell::new(&mut sink);
367        let io = RefCell::new(VmPolicyIO::new(&facts, &sink2, &self.engine, &self.ffis));
368        let ctx = CommandContext::Open(OpenContext { name });
369        let mut rs = self.machine.create_run_state(&io, ctx);
370        let status = rs.call_open(name, envelope.into());
371        match status {
372            Ok(reason) => match reason {
373                ExitReason::Normal => {
374                    let v = rs.consume_return().map_err(|e| {
375                        error!("Could not pull envelope from stack: {e}");
376                        EngineError::InternalError
377                    })?;
378                    Ok(v.try_into().map_err(|e| {
379                        error!("Envelope is not a struct: {e}");
380                        EngineError::InternalError
381                    })?)
382                }
383                ExitReason::Yield => bug!("unexpected yield"),
384                ExitReason::Check => {
385                    info!("Check {}", self.source_location(&rs));
386                    Err(EngineError::Check)
387                }
388                ExitReason::Panic => {
389                    info!("Panicked {}", self.source_location(&rs));
390                    Err(EngineError::Check)
391                }
392            },
393            Err(e) => {
394                error!("\n{e}");
395                Err(EngineError::InternalError)
396            }
397        }
398    }
399}
400
401/// [`VmPolicy`]'s actions.
402#[derive(Clone, Debug, PartialEq, Eq)]
403pub struct VmAction<'a> {
404    /// The name of the action.
405    pub name: &'a str,
406    /// The arguments of the action.
407    pub args: Cow<'a, [Value]>,
408}
409
410/// A partial version of [`VmEffect`] containing only the data. Created by
411/// [`vm_effect!`] and used to compare only the name and fields against the full
412/// `VmEffect`.
413#[derive(Debug)]
414pub struct VmEffectData {
415    /// The name of the effect.
416    pub name: String,
417    /// The fields of the effect.
418    pub fields: Vec<KVPair>,
419}
420
421impl PartialEq<VmEffect> for VmEffectData {
422    fn eq(&self, other: &VmEffect) -> bool {
423        self.name == other.name && self.fields == other.fields
424    }
425}
426
427impl PartialEq<VmEffectData> for VmEffect {
428    fn eq(&self, other: &VmEffectData) -> bool {
429        self.name == other.name && self.fields == other.fields
430    }
431}
432
433/// [`VmPolicy`]'s effects.
434#[derive(Clone, Debug, PartialEq, Eq)]
435pub struct VmEffect {
436    /// The name of the effect.
437    pub name: String,
438    /// The fields of the effect.
439    pub fields: Vec<KVPair>,
440    /// The command ID that produced this effect
441    pub command: CommandId,
442    /// Was this produced from a recall block?
443    pub recalled: bool,
444}
445
446#[derive(Copy, Clone, Debug, PartialEq)]
447enum VmPriority {
448    Basic(u32),
449    Finalize,
450}
451
452impl Default for VmPriority {
453    fn default() -> Self {
454        Self::Basic(0)
455    }
456}
457
458impl From<VmPriority> for Priority {
459    fn from(value: VmPriority) -> Self {
460        match value {
461            VmPriority::Basic(p) => Self::Basic(p),
462            VmPriority::Finalize => Self::Finalize,
463        }
464    }
465}
466
467impl<E> VmPolicy<E> {
468    fn get_command_priority(&self, name: &str) -> VmPriority {
469        debug_assert!(self.machine.command_defs.contains_key(name));
470        self.priority_map.get(name).copied().unwrap_or_default()
471    }
472}
473
474impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
475    type Action<'a> = VmAction<'a>;
476    type Effect = VmEffect;
477    type Command<'a> = VmProtocol<'a>;
478
479    fn serial(&self) -> u32 {
480        // TODO(chip): Implement an actual serial number
481        0u32
482    }
483
484    #[instrument(skip_all)]
485    fn call_rule(
486        &self,
487        command: &impl Command,
488        facts: &mut impl FactPerspective,
489        sink: &mut impl Sink<Self::Effect>,
490        recall: CommandRecall,
491    ) -> Result<(), EngineError> {
492        let unpacked: VmProtocolData<'_> = postcard::from_bytes(command.bytes()).map_err(|e| {
493            error!("Could not deserialize: {e:?}");
494            EngineError::Read
495        })?;
496        let (command_info, expected_priority) = {
497            match unpacked {
498                VmProtocolData::Init {
499                    author_id,
500                    kind,
501                    serialized_fields,
502                    signature,
503                    ..
504                } => (
505                    Some((
506                        Envelope {
507                            parent_id: CommandId::default(),
508                            author_id,
509                            command_id: command.id(),
510                            payload: Cow::Borrowed(serialized_fields),
511                            signature: Cow::Borrowed(signature),
512                        },
513                        kind,
514                        author_id,
515                    )),
516                    Priority::Init,
517                ),
518                VmProtocolData::Basic {
519                    parent,
520                    kind,
521                    author_id,
522                    serialized_fields,
523                    signature,
524                } => (
525                    Some((
526                        Envelope {
527                            parent_id: parent.id,
528                            author_id,
529                            command_id: command.id(),
530                            payload: Cow::Borrowed(serialized_fields),
531                            signature: Cow::Borrowed(signature),
532                        },
533                        kind,
534                        author_id,
535                    )),
536                    self.get_command_priority(kind).into(),
537                ),
538                // Merges always pass because they're an artifact of the graph
539                _ => (None, Priority::Merge),
540            }
541        };
542
543        if command.priority() != expected_priority {
544            error!(
545                "Expected priority {:?}, got {:?}",
546                expected_priority,
547                command.priority()
548            );
549            bug!("Command has invalid priority");
550        }
551
552        if let Some((envelope, kind, author_id)) = command_info {
553            let command_struct = self.open_command(kind, envelope.clone(), facts)?;
554            let fields: Vec<KVPair> = command_struct
555                .fields
556                .into_iter()
557                .map(|(k, v)| KVPair::new(&k, v))
558                .collect();
559            let ctx = CommandContext::Policy(PolicyContext {
560                name: kind,
561                id: command.id().into(),
562                author: author_id,
563                version: CommandId::default().into(),
564            });
565            self.evaluate_rule(kind, fields.as_slice(), envelope, facts, sink, ctx, recall)?
566        }
567        Ok(())
568    }
569
570    #[instrument(skip_all, fields(name = action.name))]
571    fn call_action(
572        &self,
573        action: Self::Action<'_>,
574        facts: &mut impl Perspective,
575        sink: &mut impl Sink<Self::Effect>,
576    ) -> Result<(), EngineError> {
577        let VmAction { name, args } = action;
578
579        let parent = match facts.head_address()? {
580            Prior::None => None,
581            Prior::Single(id) => Some(id),
582            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
583        };
584        // FIXME(chip): This is kind of wrong, but it avoids having to
585        // plumb Option<Id> into the VM and FFI
586        let ctx_parent = parent.unwrap_or_default();
587        let facts = Rc::new(RefCell::new(facts));
588        let sink = Rc::new(RefCell::new(sink));
589        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
590        let ctx = CommandContext::Action(ActionContext {
591            name,
592            head_id: ctx_parent.id.into(),
593        });
594        {
595            let mut rs = self.machine.create_run_state(&io, ctx);
596            let mut exit_reason = match args {
597                Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
598                Cow::Owned(args) => rs.call_action(name, args),
599            }
600            .map_err(|e| {
601                error!("\n{e}");
602                EngineError::InternalError
603            })?;
604            loop {
605                match exit_reason {
606                    ExitReason::Normal => {
607                        // Action completed
608                        break;
609                    }
610                    ExitReason::Yield => {
611                        // Command was published.
612                        let command_struct: Struct = rs.stack.pop().map_err(|e| {
613                            error!("should have command struct: {e}");
614                            EngineError::InternalError
615                        })?;
616
617                        let fields = command_struct
618                            .fields
619                            .iter()
620                            .map(|(k, v)| KVPair::new(k, v.clone()));
621                        io.try_borrow_mut()
622                            .assume("should be able to borrow io")?
623                            .publish(command_struct.name.clone(), fields);
624
625                        let seal_ctx = rs.get_context().seal_from_action(&command_struct.name)?;
626                        let mut rs_seal = self.machine.create_run_state(&io, seal_ctx);
627                        match rs_seal
628                            .call_seal(&command_struct.name, &command_struct)
629                            .map_err(|e| {
630                                error!("Cannot seal command: {}", e);
631                                EngineError::Panic
632                            })? {
633                            ExitReason::Normal => (),
634                            r @ ExitReason::Yield
635                            | r @ ExitReason::Check
636                            | r @ ExitReason::Panic => {
637                                error!("Could not seal command: {}", r);
638                                return Err(EngineError::Panic);
639                            }
640                        }
641
642                        // Grab sealed envelope from stack
643                        let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
644                            error!("Expected a sealed envelope {e}");
645                            EngineError::InternalError
646                        })?;
647                        let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
648                            error!("Malformed envelope: {e}");
649                            EngineError::InternalError
650                        })?;
651
652                        // The parent of a basic command should be the command that was added to the perspective on the previous
653                        // iteration of the loop
654                        let parent = match RefCell::borrow_mut(Rc::borrow(&facts)).head_address()? {
655                            Prior::None => None,
656                            Prior::Single(id) => Some(id),
657                            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
658                        };
659
660                        let priority = if parent.is_some() {
661                            self.get_command_priority(command_struct.name.as_str())
662                                .into()
663                        } else {
664                            Priority::Init
665                        };
666
667                        let data = match parent {
668                            None => VmProtocolData::Init {
669                                // TODO(chip): where does the policy value come from?
670                                policy: 0u64.to_le_bytes(),
671                                author_id: envelope.author_id,
672                                kind: &command_struct.name,
673                                serialized_fields: &envelope.payload,
674                                signature: &envelope.signature,
675                            },
676                            Some(parent) => VmProtocolData::Basic {
677                                author_id: envelope.author_id,
678                                parent,
679                                kind: &command_struct.name,
680                                serialized_fields: &envelope.payload,
681                                signature: &envelope.signature,
682                            },
683                        };
684                        let wrapped = postcard::to_allocvec(&data)?;
685                        let new_command =
686                            VmProtocol::new(&wrapped, envelope.command_id, data, priority);
687
688                        self.call_rule(
689                            &new_command,
690                            *RefCell::borrow_mut(Rc::borrow(&facts)),
691                            *RefCell::borrow_mut(Rc::borrow(&sink)),
692                            CommandRecall::None,
693                        )?;
694                        RefCell::borrow_mut(Rc::borrow(&facts))
695                            .add_command(&new_command)
696                            .map_err(|e| {
697                                error!("{e}");
698                                EngineError::Write
699                            })?;
700
701                        // After publishing a new command, the RunState's context must be updated to reflect the new head
702                        rs.update_context_with_new_head(new_command.id().into())?;
703
704                        // Resume action after last Publish
705                        exit_reason = rs.run().map_err(|e| {
706                            error!("{e}");
707                            EngineError::InternalError
708                        })?;
709                    }
710                    ExitReason::Check => {
711                        info!("Check {}", self.source_location(&rs));
712                        return Err(EngineError::Check);
713                    }
714                    ExitReason::Panic => {
715                        info!("Panicked {}", self.source_location(&rs));
716                        return Err(EngineError::Panic);
717                    }
718                };
719            }
720        }
721
722        Ok(())
723    }
724
725    fn merge<'a>(
726        &self,
727        target: &'a mut [u8],
728        ids: MergeIds,
729    ) -> Result<Self::Command<'a>, EngineError> {
730        let (left, right) = ids.into();
731        let c = VmProtocolData::Merge { left, right };
732        let data = postcard::to_slice(&c, target).map_err(|e| {
733            error!("{e}");
734            EngineError::Write
735        })?;
736        let id = CommandId::hash_for_testing_only(data);
737        Ok(VmProtocol::new(data, id, c, Priority::Merge))
738    }
739}
740
741impl fmt::Display for VmAction<'_> {
742    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
743        let mut d = f.debug_tuple(self.name);
744        for arg in self.args.as_ref() {
745            d.field(&DebugViaDisplay(arg));
746        }
747        d.finish()
748    }
749}
750
751impl fmt::Display for VmEffect {
752    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753        let mut d = f.debug_struct(&self.name);
754        for field in &self.fields {
755            d.field(field.key(), &DebugViaDisplay(field.value()));
756        }
757        d.finish()
758    }
759}
760
761/// Implements `Debug` via `T`'s `Display` impl.
762struct DebugViaDisplay<T>(T);
763
764impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
765    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
766        write!(f, "{}", self.0)
767    }
768}
769
770#[cfg(test)]
771mod test {
772    use alloc::format;
773
774    use aranya_policy_compiler::Compiler;
775    use aranya_policy_lang::lang::parse_policy_str;
776    use aranya_policy_vm::ast::Version;
777
778    use super::*;
779
780    #[test]
781    fn test_get_command_priorities() {
782        fn process(attrs: &str) -> Result<Option<VmPriority>, AttributeError> {
783            let policy = format!(
784                r#"
785                command Test {{
786                    attributes {{
787                        {attrs}
788                    }}
789                    fields {{ }}
790                    seal {{ return None }}
791                    open {{ return None }}
792                    policy {{ }}
793                }}
794                "#
795            );
796            let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
797            let module = Compiler::new(&ast)
798                .compile()
799                .unwrap_or_else(|e| panic!("{e}"));
800            let machine = Machine::from_module(module).expect("can create machine");
801            let priorities = get_command_priorities(&machine)?;
802            Ok(priorities.get("Test").copied())
803        }
804
805        assert_eq!(process(""), Ok(None));
806        assert_eq!(process("finalize: false"), Ok(None));
807
808        assert_eq!(process("priority: 42"), Ok(Some(VmPriority::Basic(42))));
809        assert_eq!(
810            process("finalize: false, priority: 42"),
811            Ok(Some(VmPriority::Basic(42)))
812        );
813        assert_eq!(
814            process("priority: 42, finalize: false"),
815            Ok(Some(VmPriority::Basic(42)))
816        );
817
818        assert_eq!(process("finalize: true"), Ok(Some(VmPriority::Finalize)));
819
820        assert_eq!(
821            process("finalize: 42"),
822            Err(AttributeError::type_mismatch(
823                "Test", "finalize", "Bool", "Int"
824            ))
825        );
826        assert_eq!(
827            process("priority: false"),
828            Err(AttributeError::type_mismatch(
829                "Test", "priority", "Int", "Bool"
830            ))
831        );
832        assert_eq!(
833            process("priority: -1"),
834            Err(AttributeError::int_range(
835                "Test",
836                "priority",
837                u32::MIN.into(),
838                u32::MAX.into(),
839            ))
840        );
841        assert_eq!(
842            process(&format!("priority: {}", i64::MAX)),
843            Err(AttributeError::int_range(
844                "Test",
845                "priority",
846                u32::MIN.into(),
847                u32::MAX.into(),
848            ))
849        );
850        assert_eq!(
851            process("finalize: true, priority: 42"),
852            Err(AttributeError::exclusive("Test", "finalize", "priority"))
853        )
854    }
855}