midenc_hir/ir/
verifier.rs

1use super::{Context, Report};
2
3/// The `OpVerifier` trait is expected to be implemented by all [Op] impls as a prequisite.
4///
5/// The actual implementation is typically generated as part of deriving [Op].
6pub trait OpVerifier {
7    fn verify(&self, context: &Context) -> Result<(), Report>;
8}
9
10/// The `Verify` trait represents verification logic associated with implementations of some trait.
11///
12/// This is specifically used for automatically deriving verification checks for [Op] impls that
13/// implement traits that imply constraints on the representation or behavior of that op.
14///
15/// For example, if some [Op] derives an op trait like `SingleBlock`, this information is recorded
16/// in the underlying [Operation] metadata, so that we can recover a trait object reference for the
17/// trait when needed. However, just deriving the trait is not sufficient to guarantee that the op
18/// actually adheres to the implicit constraints and behavior of that trait. For example,
19/// `SingleBlock` implies that the implementing op contains only regions that consist of a single
20/// [Block]. This cannot be checked statically. The first step to addressing this though, is to
21/// reify the implicit validation rules as explicit checks - hence this trait.
22///
23/// So we've established that some op traits, such as `SingleBlock` mentioned above, have implicit
24/// validation rules, and we can implement [Verify] to make the implicit validation rules of such
25/// traits explicit - but how do we ensure that when an op derives an op trait, that the [Verify]
26/// impl is also derived, _and_ that it is called when the op is verified?
27///
28/// The answer lies in the use of some tricky type-level code to accomplish the following goals:
29///
30/// * Do not emit useless checks for op traits that have no verification rules
31/// * Do not require storing data in each instance of an [Op] just to verify a trait
32/// * Do not require emitting a bunch of redundant type checks for information we know statically
33/// * Be able to automatically derive all of the verification machinery along with the op traits
34///
35/// The way this works is as follows:
36///
37/// * We `impl<T> Verify<dyn Trait> for T where T: Op` for every trait `Trait` with validation rules.
38/// * A blanket impl of [HasVerifier] exists for all `T: Verify<Trait>`. This is a marker trait used
39///   in conjunction with specialization. See the trait docs for more details on its purpose.
40/// * The [Verifier] trait provides a default vacuous impl for all `Trait` and `T` pairs. However,
41///   we also provided a specialized [Verifier] impl for all `T: Verify<Trait>` using the
42///   `HasVerifier` marker. The specialized impl applies the underlying `Verify` impl.
43/// * When deriving the op traits for an `Op` impl, we generate a hidden type that encodes all of
44///   the op traits implemented by the op. We then generate an `OpVerifier` impl for the op, which
45///   uses the hidden type we generated to reify the `Verifier` impl for each trait. The
46///   `OpVerifier` implementation uses const eval to strip out all of the vacuous verifier impls,
47///   leaving behind just the "real" verification rules specific to the traits implemented by that
48///   op.
49/// * The `OpVerifier` impl is object-safe, and is in fact a required super-trait of `Op` to ensure
50///   that verification is part of defining an `Op`, but also to ensure that `verify` is a method
51///   of `Op`, and that we can cast an `Operation` to `&dyn OpVerifier` and call `verify` on that.
52///
53/// As a result of all this, we end up with highly-specialized verifiers for each op, with no
54/// dynamic dispatch, and automatically maintained as part of the `Op` definition. When a new
55/// op trait is derived, the verifier for the op is automatically updated to verify the new trait.
56pub trait Verify<Trait: ?Sized> {
57    /// In cases where verification may be disabled via runtime configuration, or based on
58    /// dynamic properties of the type, this method can be overridden and used to signal to
59    /// the verification driver that verification should be skipped on this item.
60    #[inline(always)]
61    #[allow(unused_variables)]
62    fn should_verify(&self, context: &Context) -> bool {
63        true
64    }
65    /// Apply this verifier, but only if [Verify::should_verify] returns true.
66    #[inline]
67    fn maybe_verify(&self, context: &Context) -> Result<(), Report> {
68        if self.should_verify(context) {
69            self.verify(context)
70        } else {
71            Ok(())
72        }
73    }
74    /// Apply this verifier to the current item.
75    fn verify(&self, context: &Context) -> Result<(), Report>;
76}
77
78/// A marker trait used for verifier specialization.
79///
80/// # Safety
81///
82/// In order for the `#[rustc_unsafe_specialization_marker]` attribute to be used safely and
83/// correctly, the following rules must hold:
84///
85/// * No associated items
86/// * No impls with lifetime constraints, as specialization will ignore them
87///
88/// For our use case, which is specializing verification for a given type and trait combination,
89/// by optimizing out verification-related code for type combinations which have no verifier, these
90/// are easy rules to uphold.
91///
92/// However, we must ensure that we continue to uphold these rules moving forward.
93#[rustc_unsafe_specialization_marker]
94unsafe trait HasVerifier<Trait: ?Sized>: Verify<Trait> {}
95
96// While at first glance, it appears we would be using this to specialize on the fact that a type
97// _has_ a verifier, which is strictly-speaking true, the actual goal we're aiming to acheive is
98// to be able to identify the _absence_ of a verifier, so that we can eliminate the boilerplate for
99// verifying that trait. See `Verifier` for more information.
100unsafe impl<T, Trait: ?Sized> HasVerifier<Trait> for T where T: Verify<Trait> {}
101
102/// The `Verifier` trait is used to derive a verifier for a given trait and concrete type.
103///
104/// It does this by providing a default implementation for all combinations of `Trait` and `T`,
105/// which always succeeds, and then specializing that implementation for `T: HasVerifier<Trait>`.
106///
107/// This has the effect of making all traits "verifiable", but only actually doing any verification
108/// for types which implement `Verify<Trait>`.
109///
110/// We go a step further and actually set things up so that `rustc` can eliminate all of the dead
111/// code when verification is vacuous. This is done by using const eval in the hidden type generated
112/// for an [Op] impls [OpVerifier] implementation, which will wrap verification in a const-evaluated
113/// check of the `VACUOUS` associated const. It can also be used directly, but the general idea
114/// behind all of this is that we don't need to directly touch any of this, it's all generated.
115///
116/// NOTE: Because this trait provides a default blanket impl for all `T`, you should avoid bringing
117/// it into scope unless absolutely needed. It is virtually always preferred to explicitly invoke
118/// this trait using turbofish syntax, so as to avoid conflict with the [Verify] trait, and to
119/// avoid polluting the namespace for all types in scope.
120pub trait Verifier<Trait: ?Sized> {
121    /// An implementation of `Verifier` sets this flag to true when its implementation is vacuous,
122    /// i.e. it always succeeds and is not dependent on runtime context.
123    ///
124    /// The default implementation of this trait sets this to `true`, since without a verifier for
125    /// the type, verification always succeeds. However, we can specialize on the presence of
126    /// a verifier and set this to `false`, which will result in all of the verification logic
127    /// being applied.
128    ///
129    /// ## Example Usage
130    ///
131    /// Shown below is an example of how one can use const eval to eliminate dead code branches
132    /// in verifier selection, so that the resulting implementation is specialized and able to
133    /// have more optimizations applied as a result.
134    ///
135    /// ```rust
136    /// use midenc_hir::{Context, Report, Verify, verifier::Verifier};
137    ///
138    /// /// This trait is a marker for a type that should never validate, i.e. verifying it always
139    /// /// returns an error.
140    /// trait Nope {}
141    /// impl<T: Nope> Verify<dyn Nope> for Any<T> {
142    ///     fn verify(&self, context: &Context) -> Result<(), Report> {
143    ///         Err(Report::msg("nope"))
144    ///     }
145    /// }
146    ///
147    /// /// We can't impl the `Verify` trait for all T outside of `midenc_hir`, so we newtype all T
148    /// /// to do so, in effect, we can mimic the effect of implementing for all T using this type.
149    /// struct Any<T>(core::marker::PhantomData<T>);
150    /// impl<T> Any<T> {
151    ///     fn new() -> Self {
152    ///         Self(core::marker::PhantomData)
153    ///     }
154    /// }
155    ///
156    /// /// This struct implements `Nope`, so it has an explicit verifier, which always fails
157    /// struct AlwaysRejected;
158    /// impl Nope for AlwaysRejected {}
159    ///
160    /// /// This struct doesn't implement `Nope`, so it gets a vacuous verifier, which always
161    /// /// succeeds.
162    /// struct AlwaysAccepted;
163    ///
164    /// /// Our vacuous verifier impl
165    /// #[inline(always)]
166    /// fn noop<T>(_: &Any<T>, _: &Context) -> Result<(), Report> { Ok(()) }
167    ///
168    /// /// This block uses const-eval to select the verifier for Any<AlwaysAccepted> statically
169    /// let always_accepted = const {
170    ///     if <Any<AlwaysAccepted> as Verifier<dyn Nope>>::VACUOUS {
171    ///        noop
172    ///     } else {
173    ///        <Any<AlwaysAccepted> as Verifier<dyn Nope>>::maybe_verify
174    ///     }
175    /// };
176    ///
177    /// /// This block uses const-eval to select the verifier for Any<AlwaysRejected> statically
178    /// let always_rejected = const {
179    ///     if <Any<AlwaysRejected> as Verifier<dyn Nope>>::VACUOUS {
180    ///        noop
181    ///     } else {
182    ///        <Any<AlwaysRejected> as Verifier<dyn Nope>>::maybe_verify
183    ///     }
184    /// };
185    ///
186    /// /// Verify that we got the correct impls. We can't verify that all of the abstraction was
187    /// /// eliminated, but from reviewing the assembly output, it appears that this is precisely
188    /// /// what happens.
189    /// let context = Context::default();
190    /// assert!(always_accepted(&Any::new(), &context).is_ok());
191    /// assert!(always_rejected(&Any::new(), &context).is_err());
192    /// ```
193    const VACUOUS: bool;
194
195    /// Checks if this verifier is applicable for the current item
196    fn should_verify(&self, context: &Context) -> bool;
197    /// Applies the verifier for this item, if [Verifier::should_verify] returns `true`
198    fn maybe_verify(&self, context: &Context) -> Result<(), Report>;
199    /// Applies the verifier for this item
200    fn verify(&self, context: &Context) -> Result<(), Report>;
201}
202
203/// The default blanket impl for all types and traits
204impl<T, Trait: ?Sized> Verifier<Trait> for T {
205    default const VACUOUS: bool = true;
206
207    #[inline(always)]
208    default fn should_verify(&self, _context: &Context) -> bool {
209        false
210    }
211
212    #[inline(always)]
213    default fn maybe_verify(&self, _context: &Context) -> Result<(), Report> {
214        Ok(())
215    }
216
217    #[inline(always)]
218    default fn verify(&self, _context: &Context) -> Result<(), Report> {
219        Ok(())
220    }
221}
222
223/// THe specialized impl for types which implement `Verify<Trait>`
224impl<T, Trait: ?Sized> Verifier<Trait> for T
225where
226    T: HasVerifier<Trait>,
227{
228    const VACUOUS: bool = false;
229
230    #[inline]
231    fn should_verify(&self, context: &Context) -> bool {
232        <T as Verify<Trait>>::should_verify(self, context)
233    }
234
235    #[inline(always)]
236    fn maybe_verify(&self, context: &Context) -> Result<(), Report> {
237        <T as Verify<Trait>>::maybe_verify(self, context)
238    }
239
240    #[inline]
241    fn verify(&self, context: &Context) -> Result<(), Report> {
242        <T as Verify<Trait>>::verify(self, context)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use core::hint::black_box;
249
250    use super::*;
251    use crate::{traits::SingleBlock, Operation};
252
253    struct Vacuous;
254
255    /// In this test, we're validating that a type that trivially verifies specializes as vacuous,
256    /// and that a type we know has a "real" verifier, specializes as _not_ vacuous
257    #[test]
258    fn verifier_specialization_concrete() {
259        assert!(black_box(<Vacuous as Verifier<dyn SingleBlock>>::VACUOUS));
260        assert!(black_box(!<Operation as Verifier<dyn SingleBlock>>::VACUOUS));
261    }
262}