Skip to main content

midenc_hir/ir/
verifier.rs

1use super::{Context, Report};
2
3/// The `OpVerifier` trait is expected to be implemented by all [crate::Op] impls as a prequisite.
4///
5/// The actual implementation is typically generated as part of deriving [crate::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 [crate::Op] impls
13/// that implement traits that imply constraints on the representation or behavior of that op.
14///
15/// For example, if some [crate::Op] derives an op trait like `SingleBlock`, this information is
16/// recorded in the underlying [crate::Operation] metadata, so that we can recover a trait object
17/// reference for the trait when needed. However, just deriving the trait is not sufficient to
18/// guarantee that the op actually adheres to the implicit constraints and behavior of that trait.
19/// For example, `SingleBlock` implies that the implementing op contains only regions that consist
20/// of a single [crate::Block]. This cannot be checked statically. The first step to addressing this
21/// though, is to 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 [crate::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]
94pub unsafe 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 [crate::Op] impls [OpVerifier] implementation, which will wrap verification in a
113/// const-evaluated check of the `VACUOUS` associated const. It can also be used directly, but the
114/// general idea behind all of this is that we don't need to directly touch any of this, it's all
115/// generated.
116///
117/// NOTE: Because this trait provides a default blanket impl for all `T`, you should avoid bringing
118/// it into scope unless absolutely needed. It is virtually always preferred to explicitly invoke
119/// this trait using turbofish syntax, so as to avoid conflict with the [Verify] trait, and to
120/// avoid polluting the namespace for all types in scope.
121pub trait Verifier<Trait: ?Sized> {
122    /// An implementation of `Verifier` sets this flag to true when its implementation is vacuous,
123    /// i.e. it always succeeds and is not dependent on runtime context.
124    ///
125    /// The default implementation of this trait sets this to `true`, since without a verifier for
126    /// the type, verification always succeeds. However, we can specialize on the presence of
127    /// a verifier and set this to `false`, which will result in all of the verification logic
128    /// being applied.
129    ///
130    /// ## Example Usage
131    ///
132    /// Shown below is an example of how one can use const eval to eliminate dead code branches
133    /// in verifier selection, so that the resulting implementation is specialized and able to
134    /// have more optimizations applied as a result.
135    ///
136    /// ```rust
137    /// use midenc_hir::{Context, Report, Verify, verifier::Verifier};
138    ///
139    /// /// This trait is a marker for a type that should never validate, i.e. verifying it always
140    /// /// returns an error.
141    /// trait Nope {}
142    /// impl<T: Nope> Verify<dyn Nope> for Any<T> {
143    ///     fn verify(&self, context: &Context) -> Result<(), Report> {
144    ///         Err(Report::msg("nope"))
145    ///     }
146    /// }
147    ///
148    /// /// We can't impl the `Verify` trait for all T outside of `midenc_hir`, so we newtype all T
149    /// /// to do so, in effect, we can mimic the effect of implementing for all T using this type.
150    /// struct Any<T>(core::marker::PhantomData<T>);
151    /// impl<T> Any<T> {
152    ///     fn new() -> Self {
153    ///         Self(core::marker::PhantomData)
154    ///     }
155    /// }
156    ///
157    /// /// This struct implements `Nope`, so it has an explicit verifier, which always fails
158    /// struct AlwaysRejected;
159    /// impl Nope for AlwaysRejected {}
160    ///
161    /// /// This struct doesn't implement `Nope`, so it gets a vacuous verifier, which always
162    /// /// succeeds.
163    /// struct AlwaysAccepted;
164    ///
165    /// /// Our vacuous verifier impl
166    /// #[inline(always)]
167    /// fn noop<T>(_: &Any<T>, _: &Context) -> Result<(), Report> { Ok(()) }
168    ///
169    /// /// This block uses const-eval to select the verifier for Any<AlwaysAccepted> statically
170    /// let always_accepted = const {
171    ///     if <Any<AlwaysAccepted> as Verifier<dyn Nope>>::VACUOUS {
172    ///        noop
173    ///     } else {
174    ///        <Any<AlwaysAccepted> as Verifier<dyn Nope>>::maybe_verify
175    ///     }
176    /// };
177    ///
178    /// /// This block uses const-eval to select the verifier for Any<AlwaysRejected> statically
179    /// let always_rejected = const {
180    ///     if <Any<AlwaysRejected> as Verifier<dyn Nope>>::VACUOUS {
181    ///        noop
182    ///     } else {
183    ///        <Any<AlwaysRejected> as Verifier<dyn Nope>>::maybe_verify
184    ///     }
185    /// };
186    ///
187    /// /// Verify that we got the correct impls. We can't verify that all of the abstraction was
188    /// /// eliminated, but from reviewing the assembly output, it appears that this is precisely
189    /// /// what happens.
190    /// let context = Context::default();
191    /// assert!(always_accepted(&Any::new(), &context).is_ok());
192    /// assert!(always_rejected(&Any::new(), &context).is_err());
193    /// ```
194    const VACUOUS: bool;
195
196    /// Checks if this verifier is applicable for the current item
197    fn should_verify(&self, context: &Context) -> bool;
198    /// Applies the verifier for this item, if [Verifier::should_verify] returns `true`
199    fn maybe_verify(&self, context: &Context) -> Result<(), Report>;
200    /// Applies the verifier for this item
201    fn verify(&self, context: &Context) -> Result<(), Report>;
202}
203
204/// The default blanket impl for all types and traits
205impl<T, Trait: ?Sized> Verifier<Trait> for T {
206    default const VACUOUS: bool = true;
207
208    #[inline(always)]
209    default fn should_verify(&self, _context: &Context) -> bool {
210        false
211    }
212
213    #[inline(always)]
214    default fn maybe_verify(&self, _context: &Context) -> Result<(), Report> {
215        Ok(())
216    }
217
218    #[inline(always)]
219    default fn verify(&self, _context: &Context) -> Result<(), Report> {
220        Ok(())
221    }
222}
223
224/// THe specialized impl for types which implement `Verify<Trait>`
225impl<T, Trait: ?Sized> Verifier<Trait> for T
226where
227    T: HasVerifier<Trait>,
228{
229    const VACUOUS: bool = false;
230
231    #[inline]
232    fn should_verify(&self, context: &Context) -> bool {
233        <T as Verify<Trait>>::should_verify(self, context)
234    }
235
236    #[inline(always)]
237    fn maybe_verify(&self, context: &Context) -> Result<(), Report> {
238        <T as Verify<Trait>>::maybe_verify(self, context)
239    }
240
241    #[inline]
242    fn verify(&self, context: &Context) -> Result<(), Report> {
243        <T as Verify<Trait>>::verify(self, context)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use core::hint::black_box;
250
251    use super::*;
252    use crate::{Operation, traits::SingleBlock};
253
254    struct Vacuous;
255
256    /// In this test, we're validating that a type that trivially verifies specializes as vacuous,
257    /// and that a type we know has a "real" verifier, specializes as _not_ vacuous
258    #[test]
259    fn verifier_specialization_concrete() {
260        assert!(black_box(<Vacuous as Verifier<dyn SingleBlock>>::VACUOUS));
261        assert!(black_box(!<Operation as Verifier<dyn SingleBlock>>::VACUOUS));
262    }
263}