Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generated Rust Code

An ASN.1 definition such as Packet becomes a Rust value type Packet and a verified nominal format type PACKET, used as PACKET::Fmt. The screaming-case format name avoids colliding with the idiomatic value name.

SEQUENCE and supported heterogeneous SET definitions become structs; CHOICE becomes an enum carrying the alternatives; and ENUMERATED becomes a closed typed enum. Anonymous composites receive deterministic private helper definitions. IMPLICIT tagging replaces the outer tag; untagged CHOICE and ANY have no single tag to replace, so the generator tags those explicitly.

The examples below come from the module in the tutorial:

Kind   ::= ENUMERATED { request(0), response(1) }
Packet ::= SEQUENCE {
    kind    Kind,
    payload [0] IMPLICIT OCTET STRING (SIZE (1..32)) OPTIONAL
}

The shape of an emitted file

Unlike a DSL-generated module, an ASN.1 module has no banner sections. It is one flat verus! block holding every definition’s types and schema, followed by one small private module per definition outside the block:

use vest_lib::asn1::der::{...};          // preamble selected by the encoding rules
verus! {

    // per definition: value type, abstract type, predicates, mappers,
    // combinator representations, and the nominal format type

} // verus!

mod __impl_kind   { use super::*; vest_lib::impl_der!(...); }
mod __impl_packet { use super::*; vest_lib::impl_der!(...); }

That last part is the biggest structural difference. Where the DSL writes out derived_specs, derived_proofs, and exec_impls explicitly, the ASN.1 backend emits one macro call per definition and lets vest_lib expand the specifications, proofs, and executable code.

Value and abstract types

This matches the DSL closely — a value type and a generic abstract type, bridged by DeepView:

pub struct Packet<'a> {
    pub kind: Kind,
    pub payload: Option<&'a [u8]>,
}

#[verifier::ext_equal]
pub struct PacketSpec<T0 = KindSpec, T1 = Option<Seq<u8>>> {
    pub kind: T0,
    pub payload: T1,
}

impl<'a> DeepView for Packet<'a> {
    type V = PacketSpec;
    #[verifier::opaque]
    open spec fn deep_view(&self) -> Self::V { /* field-wise */ }
}

For a definition whose value type is already Copy and structural — an ENUMERATED, for instance — there is no separate abstract type at all.

#[repr(i16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StructuralEq)]
pub enum Kind { Request = 0, Response = 1 }

pub type KindSpec = Kind; // the abstract type is the same as the value type

impl DeepView for Kind { type V = Self; /* *self */ }

Predicates and mappers

ASN.1 constraints become named predicate structs used in both the specification and implementation, rather than the inline closures the DSL emits:

#[derive(Clone, Copy)]
pub struct KindPredicate;

impl SpecPred<i16> for KindPredicate { /* value == 0 || value == 1 */ }
impl Pred<i16>     for KindPredicate { /* the executable test */ }

ENUMERATED values are built on the verified i16 integer-content backend, which is why the predicate is over i16. Larger enumerations are currently not yet supported.

Mappers follow the DSL’s Forward/Reverse pattern, with one addition — they implement the executable Map as well as SpecMap.

pub struct KindForward;
pub struct KindReverse;

impl SpecMap for KindForward { /* .. */ }
impl SpecMap for KindReverse { /* .. */ }
impl Map<i16> for KindForward { /* executable */ }
impl Map<Kind> for KindReverse { /* executable */ }

The nominal format type

/// DER format for ASN.1 `Kind`.
type KIND__ = Mapped<Refined<Enumerated16TlvFmt, KindPredicate>, BiMap<KindForward, KindReverse>>;

#[derive(Clone, Copy)]
#[verifier::ext_equal]
pub struct KIND(pub Class, pub u64); // tag class, tag number

impl KIND {
    pub const Fmt: Self = Self(Class::Universal, 10u64);

    #[verifier::allow_in_spec]
    pub const fn schema() -> KIND__
        returns (Mapped { inner: Refined(ENUMERATED16, KindPredicate),
                          mapper: BiMap(KindForward, KindReverse) }),
    { /* the same representation */ }

    proof fn lemma_schema_unambiguous(&self) { /* .. */ }
}

Several things differ from the DSL here.

The format type is always parametric. MessageFmt in a DSL module is a zero-sized struct. KIND is a tuple struct holding the effective tag class and number, so an IMPLICIT tag can replace the outer tag without rebuilding the format. KIND::Fmt is the associated constant carrying the definition’s own tag — Class::Universal, 10 for ENUMERATED, 16 for SEQUENCE, etc.

schema() is dual spec-exec. The DSL’s spec_inner() is pub open spec fn — specification only. schema() here is a const fn marked #[verifier::allow_in_spec] with a returns clause, so the same definition serves both worlds (there are valid reasons for the DSL to separate them, such as to allow for more flexible executable implementations).

lemma_schema_unambiguous has no DSL counterpart. It proves the ASN.1 schema unambiguous (needed for serialize-then-parse roundtrip) by leveraging a sound over-approximation of each ASN.1 combinator’s parsing domain. On the other hand, the DSL’s constructs disambiguate themselves largely by construction, so the DSL does not need to emit this lemma.

Specifications, proofs, and executable code

Everything else arrives from one macro invocation, placed outside verus!:

mod __impl_packet {
    use super::*;
    vest_lib::impl_der!(
        tagged_exact(true),   // this definition contributes an outer tag
        borrowed,             // the value type borrows from the input
        PACKET, PACKET__,     // nominal type and its combinator type alias
        PacketSpec, Packet,   // abstract and executable value types
        PacketForward, PacketReverse
    );
}

impl_der! (or impl_ber! under BER) expands to the same code the DSL emits explicitly — derived spec trait impls (SpecParser, SpecSerializer, SpecByteLen, Consistency) and derived proof trait impls (SafeParser, SoundParser, Productive, NonTailFmt, GoodSerializer, NonMalleable, etc.). Additionally, it also expands to the executable trait impls (Parser, Prepare, Serializer).

Calling it

Calling the parser, serializer, and the prepare methods is almost identical in shape to DSL-generated code, except that the format value is the ::Fmt constant:

use vest_lib::core::exec::{Parser, Prepare, SerializerExt};

let (consumed, packet) = PACKET::Fmt.parse(&encoded).unwrap();

let size = PACKET::Fmt.prepare(&packet).unwrap();
let mut output = vec![0u8; size];
PACKET::Fmt.serialize(&packet, &mut output);