Skip to content

Commit

Permalink
[derive] Overhaul repr parsing
Browse files Browse the repository at this point in the history
Represent the result of parsing all `#[repr(...)]` attributes on a type
as a high-level type which is only capable of representing valid
combinations of `#[repr(...)]` attributes and processes them into a
concise representation that's easier for high-level code to work with.

This prepares us to more easily fix #1748
  • Loading branch information
joshlf committed Sep 25, 2024
1 parent 6b7a012 commit 1966df2
Show file tree
Hide file tree
Showing 2 changed files with 376 additions and 0 deletions.
1 change: 1 addition & 0 deletions zerocopy-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mod ext;
#[cfg(test)]
mod output_tests;
mod repr;
mod repr2;

use proc_macro2::{TokenStream, TokenTree};
use quote::ToTokens;
Expand Down
375 changes: 375 additions & 0 deletions zerocopy-derive/src/repr2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
// Copyright 2019 The Fuchsia Authors
//
// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

use core::{
convert::{Infallible, TryFrom},
num::NonZeroU64,
};

use proc_macro2::Span;
use syn::{
punctuated::Punctuated, spanned::Spanned as _, token::Comma, Attribute, DeriveInput, Error,
LitInt, Meta,
};

/// The computed representation of a type.
///
/// This is the result of processing all `#[repr(...)]` attributes on a type, if
/// any. A `Repr` is only capable of representing legal combinations of
/// `#[repr(...)]` attributes.
enum Repr<Prim, Packed> {
/// `#[repr(transparent)]`
Transparent(Span),
/// A compound representation: `repr(C)`, `repr(Rust)`, or `repr(Int)`
/// optionally combined with `repr(packed(...))` or `repr(align(...))`
Compound(Spanned<CompoundRepr<Prim>>, Option<Spanned<AlignRepr<Packed>>>),
}

/// A compound representation: `repr(C)`, `repr(Rust)`, or `repr(Int)`.
enum CompoundRepr<Prim> {
C,
Rust,
Primitive(Prim),
}

/// `repr(Int)`
enum PrimitiveRepr {
U8,
U16,
U32,
U64,
Usize,
I8,
I16,
I32,
I64,
Isize,
}

/// `repr(packed(...))` or `repr(align(...))`
enum AlignRepr<Packed> {
Packed(Packed),
Align(NonZeroU64),
}

/// The representations which can legally appear on a struct type.
type StructRepr = Repr<Infallible, NonZeroU64>;

/// The representations which can legally appear on a union type.
type UnionRepr = Repr<Infallible, NonZeroU64>;

/// The representations which can legally appear on an enum type.
type EnumRepr = Repr<PrimitiveRepr, Infallible>;

/// The result of parsing a single `#[repr(...)]` attribute or a single
/// directive inside a compound `#[repr(..., ...)]` attribute.
#[derive(Copy, Clone, PartialEq, Eq)]
enum RawRepr {
Transparent,
C,
Rust,
U8,
U16,
U32,
U64,
Usize,
I8,
I16,
I32,
I64,
Isize,
Align(NonZeroU64),
PackedN(NonZeroU64),
Packed,
}

/// A value with an associated span.
#[derive(Copy, Clone)]
struct Spanned<T> {
t: T,
span: Span,
}

impl<T> Spanned<T> {
fn new(t: T, span: Span) -> Spanned<T> {
Spanned { t, span }
}

/// Delegates to `T: TryFrom`, preserving span information in both the
/// success and error cases.
fn try_from<E, U>(u: Spanned<U>) -> Result<Spanned<T>, FromRawReprError<Spanned<E>>>
where
T: TryFrom<U, Error = FromRawReprError<E>>,
{
let Spanned { t: u, span } = u;
T::try_from(u).map(|t| Spanned { t, span }).map_err(|err| match err {
FromRawReprError::None => FromRawReprError::None,
FromRawReprError::Err(e) => FromRawReprError::Err(Spanned::new(e, span)),
})
}
}

/// The error from converting from a `RawRepr`.
enum FromRawReprError<E> {
/// The `RawRepr` doesn't affect the high-level repr we're parsing (e.g.
/// it's `align(...)` and we're parsing a `CompoundRepr`).
None,
/// The `RawRepr` is invalid for the high-level repr we're parsing (e.g.
/// it's `packed` repr and we're parsing an `AlignRepr` for an enum type).
Err(E),
}

impl TryFrom<RawRepr> for PrimitiveRepr {
type Error = ();
fn try_from(raw: RawRepr) -> Result<PrimitiveRepr, ()> {
use RawRepr::*;
match raw {
U8 => Ok(PrimitiveRepr::U8),
U16 => Ok(PrimitiveRepr::U16),
U32 => Ok(PrimitiveRepr::U32),
U64 => Ok(PrimitiveRepr::U64),
Usize => Ok(PrimitiveRepr::Usize),
I8 => Ok(PrimitiveRepr::I8),
I16 => Ok(PrimitiveRepr::I16),
I32 => Ok(PrimitiveRepr::I32),
I64 => Ok(PrimitiveRepr::I64),
Isize => Ok(PrimitiveRepr::Isize),
Transparent | C | Rust | Align(_) | PackedN(_) | Packed => Err(()),
}
}
}

impl TryFrom<RawRepr> for Infallible {
type Error = ();
fn try_from(raw: RawRepr) -> Result<Infallible, ()> {
Err(())
}
}

impl<Prim: TryFrom<RawRepr, Error = ()>> TryFrom<RawRepr> for CompoundRepr<Prim> {
type Error = FromRawReprError<()>;
fn try_from(raw: RawRepr) -> Result<CompoundRepr<Prim>, FromRawReprError<()>> {
use RawRepr::*;
match raw {
C => Ok(CompoundRepr::C),
Rust => Ok(CompoundRepr::Rust),
raw @ (U8 | U16 | U32 | U64 | Usize | I8 | I16 | I32 | I64 | Isize) => {
Prim::try_from(raw)
.map(CompoundRepr::Primitive)
.map_err(|()| FromRawReprError::Err(()))
}
Transparent | Align(_) | PackedN(_) | Packed => Err(FromRawReprError::None),
}
}
}

impl TryFrom<RawRepr> for NonZeroU64 {
type Error = ();
fn try_from(raw: RawRepr) -> Result<NonZeroU64, ()> {
use RawRepr::*;
match raw {
Packed => Ok(NonZeroU64::MIN),
PackedN(n) => Ok(n),
U8 | U16 | U32 | U64 | Usize | I8 | I16 | I32 | I64 | Isize | Transparent | C
| Rust | Align(_) => Err(()),
}
}
}

impl<Pcked: TryFrom<RawRepr, Error = ()>> TryFrom<RawRepr> for AlignRepr<Pcked> {
type Error = FromRawReprError<()>;
fn try_from(raw: RawRepr) -> Result<AlignRepr<Pcked>, FromRawReprError<()>> {
use RawRepr::*;
match raw {
Packed | PackedN(_) => {
Pcked::try_from(raw).map(AlignRepr::Packed).map_err(|()| FromRawReprError::Err(()))
}
Align(n) => Ok(AlignRepr::Align(n)),
U8 | U16 | U32 | U64 | Usize | I8 | I16 | I32 | I64 | Isize | Transparent | C
| Rust => Err(FromRawReprError::None),
}
}
}

/// The error from extracting a high-level repr type from a list of `RawRepr`s.
enum FromRawReprsError<E> {
/// None of the `RawRepr`s affect the high-level repr we're parsing (e.g.
/// it's a single `align(...)` and we're parsing a `CompoundRepr`).
None,
/// One of the `RawRepr`s is invalid for the high-level repr we're parsing
/// (e.g. there's a `packed` repr and we're parsing an `AlignRepr` for an
/// enum type).
Single(E),
/// Two `RawRepr`s appear which both affect the high-level repr we're
/// parsing (e.g., the list is `#[repr(align(2), packed)]`). Note that we
/// conservatively treat redundant reprs as conflicting (e.g.
/// `#[repr(packed, packed)]`).
Conflict,
}

impl<E> From<Spanned<FromRawReprsError<E>>> for Error {
fn from(err: Spanned<FromRawReprsError<E>>) -> Error {
todo!()
}
}

/// Tries to extract a high-level repr from a list of `RawRepr`s.
fn try_from_raw_reprs<'a, E, R: TryFrom<RawRepr, Error = FromRawReprError<E>>>(
r: impl IntoIterator<Item = &'a Spanned<RawRepr>>,
) -> Result<Option<Spanned<R>>, Spanned<FromRawReprsError<E>>> {
// Walk the list of `RawRepr`s and attempt to convert each to an `R`. Bail
// if we find any errors. If we find more than one which converts to an `R`,
// bail with a `Conflict` error.
r.into_iter().try_fold(None, |found, raw| {
let new = match Spanned::<R>::try_from(*raw) {
Ok(r) => r,
// This `RawRepr` doesn't convert to an `R`, so keep the current
// found `R`, if any.
Err(FromRawReprError::None) => return Ok(found),
// This repr is unsupported for the decorated type (e.g.
// `repr(packed)` on an enum).
Err(FromRawReprError::Err(Spanned { t: err, span })) => {
return Err(Spanned::new(FromRawReprsError::Single(err), span))
}
};

if found.is_some() {
// We already found an `R`, but this `RawRepr` also converts to an
// `R`, so that's a conflict.
Err(Spanned::new(FromRawReprsError::Conflict, new.span))
} else {
Ok(Some(new))
}
})
}

impl<Prim, Packed> Repr<Prim, Packed> {
pub(crate) fn parse(input: &DeriveInput) -> Result<Repr<Prim, Packed>, Error>
where
Prim: TryFrom<RawRepr, Error = ()>,
Packed: TryFrom<RawRepr, Error = ()>,
{
let raw_reprs = RawRepr::from_attrs(&input.attrs)?;

let transparent = {
let mut transparents = raw_reprs.iter().filter_map(|Spanned { t, span }| match t {
RawRepr::Transparent => Some(span),
_ => None,
});
let first = transparents.next();
let second = transparents.next();
match (first, second) {
(None, None) => None,
(Some(span), None) => Some(*span),
(Some(_), Some(second)) => {
return Err(Spanned::new(FromRawReprsError::<()>::Conflict, *second).into())
}
// An iterator can't produce a value only on the second call to
// `.next()`.
(None, Some(_)) => unreachable!(),
}
};

let compound: Option<Spanned<CompoundRepr<Prim>>> = try_from_raw_reprs(raw_reprs.iter())?;
let align: Option<Spanned<AlignRepr<Packed>>> = try_from_raw_reprs(raw_reprs.iter())?;

if let Some(span) = transparent {
if compound.is_some() || align.is_some() {
// TODO: Error: conflicting representation hints
todo!()
}

Ok(Repr::Transparent(span))
} else {
Ok(Repr::Compound(
compound.unwrap_or(Spanned::new(CompoundRepr::Rust, Span::call_site())),
align,
))
}
}
}

impl RawRepr {
fn from_attrs(attrs: &[Attribute]) -> Result<Vec<Spanned<RawRepr>>, Error> {
let mut reprs = Vec::new();
for attr in attrs {
// Ignore documentation attributes.
if attr.path().is_ident("doc") {
continue;
}
if let Meta::List(ref meta_list) = attr.meta {
if meta_list.path.is_ident("repr") {
let parsed: Punctuated<Meta, Comma> =
match meta_list.parse_args_with(Punctuated::parse_terminated) {
Ok(parsed) => parsed,
Err(_) => {
return Err(Error::new_spanned(
&meta_list.tokens,
"unrecognized representation hint",
))
}
};
for meta in parsed {
reprs.push(RawRepr::from_meta(&meta)?);
// match R::parse(&meta) {
// Ok(repr) => reprs.push((meta, repr)),
// Err(err) => errors.push(err),
// }
}
}
}
}

Ok(reprs)
}

fn from_meta(meta: &Meta) -> Result<Spanned<RawRepr>, Error> {
let (path, list) = match meta {
Meta::Path(path) => (path, None),
Meta::List(list) => (&list.path, Some(list)),
_ => return Err(Error::new_spanned(meta, "unrecognized representation hint")),
};

let ident = path
.get_ident()
.ok_or_else(|| Error::new_spanned(meta, "unrecognized representation hint"))?;

use RawRepr::*;
let repr = match (ident.to_string().as_str(), list) {
("u8", None) => U8,
("u16", None) => U16,
("u32", None) => U32,
("u64", None) => U64,
("usize", None) => Usize,
("i8", None) => I8,
("i16", None) => I16,
("i32", None) => I32,
("i64", None) => I64,
("isize", None) => Isize,
("C", None) => C,
("transparent", None) => Transparent,
("packed", None) => Packed,
("packed", Some(list)) => {
PackedN(list.parse_args::<LitInt>()?.base10_parse::<NonZeroU64>()?)
}
("align", Some(list)) => {
Align(list.parse_args::<LitInt>()?.base10_parse::<NonZeroU64>()?)
}
_ => return Err(Error::new_spanned(meta, "unrecognized representation hint")),
};

Ok(Spanned::new(repr, meta.span()))
}
}

#[cfg(test)]
mod tests {
// TODO: Test `try_from_raw_reprs` aggressively and all of the trait impls
// that it depends on. Do this for every type parameter (NonZeroU64,
// PrimitiveRepr, and Infallible).
}

0 comments on commit 1966df2

Please sign in to comment.