Skip to content

Commit

Permalink
Add ReflectDsl for reflection-based ParseDsl
Browse files Browse the repository at this point in the history
We don't really need our own macro to derive ParseDsl. We can take
advantage of `Struct` to "discover" field names AND parsers for the
fields in question.

Then, since the chirp interpreter calls `ParseDsl::method` with the
method name, we can use `Struct::field_mut` to get the field we want.
We can then use the bevy reflect serialization system to parse the
passed args to get a `Box<dyn Reflect>`. The only thing left to do is
set the field value with `Reflect::set`.

This is implemented as a wrapper type `ReflectDsl<B, D, F>`. Each type
parameter has a function:

- `B`: The `Reflect` type we wrap. It is also a `Bundle`, since we want
  to insert it at the end.
- `D`: The "child DSL", works exactly like all the other DSLs
- `F`: The formatter. Bevy's deserialization depends on a Deserializer,
  which changes based on the file type. By default we use the Ron
  deserializer, but it is possible to swap it for something else.
  This is what is used to parse the arguments passed to the methods for
  `ReflectDsl`.

Incidentatly, this also enables us to turn any Bundle into a DslBundle!
Which is something I wanted to add for a while now.

Fixes #57
Fixes #27
  • Loading branch information
nicopap committed Aug 25, 2023
1 parent 072566a commit 7062948
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 2 deletions.
2 changes: 2 additions & 0 deletions chirp/src/interpret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ impl<'w, 's, 'a, 'h, 'l, 'll, 'r, D: ParseDsl> Interpreter<'w, 's, 'a, 'h, 'l, '
type Swar = u32;
const LANES: usize = 8;
const SWAR_BYTES: usize = (Swar::BITS / 8) as usize;

#[allow(clippy::verbose_bit_mask)] // what a weird lint
fn contains_swar(mut xored: Swar) -> bool {
// For a position, nothing easier: pos = 0; pos += ret; ret &= xored & 0xff != 0;
let mut ret = false;
Expand Down
3 changes: 2 additions & 1 deletion chirp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ pub use cuicui_chirp_macros::parse_dsl_impl;
pub use interpret::{Handles, InterpError};
pub use loader::spawn::{Chirp, ChirpInstances};
pub use parse::ParseDsl;
pub use reflect::ReflectDsl;

pub mod interpret;
pub mod loader;
pub mod parse;
// pub mod reflect;
pub mod reflect;
pub mod wrapparg;

#[doc(hidden)]
Expand Down
1 change: 1 addition & 0 deletions chirp/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ impl<D> DslParseError<D> {
}

/// Argument to [`ParseDsl::method`].
#[derive(Clone, Copy)]
pub struct MethodCtx<'a, 'l, 'll, 'r> {
/// The method name.
pub name: &'a str,
Expand Down
203 changes: 203 additions & 0 deletions chirp/src/reflect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! [`ReflectDsl`] and helper types.
//!
//! Instead of using [`ParseDsl`]
use std::{any::type_name, convert::Infallible, fmt, marker::PhantomData};

use anyhow::Result;
use bevy::ecs::prelude::{Bundle, Entity};
use bevy::prelude::{Deref, DerefMut};
use bevy::reflect::erased_serde::__private::serde::de::DeserializeSeed;
use bevy::reflect::{serde::TypedReflectDeserializer, Reflect, Struct};
use cuicui_dsl::DslBundle;
use thiserror::Error;

use crate::parse::{MethodCtx, ParseDsl};

/// Occurs in [`ReflectDsl::typed_method`].
#[derive(Error)]
enum ReflectDslError<T> {
#[error(
"Tried to set the field '{0}' of ReflectDsl<{ty}>, but {ty} \
doesn't have such a field",
ty=type_name::<T>()
)]
BadField(String),
#[error(
"The field {path} of '{ty}' is not registered. \
Please register the type '{missing}' to be able to use ReflectDsl<{ty}>.",
ty=type_name::<T>(),
)]
NotRegistered { path: String, missing: String },
#[error("Failed to deserialize ReflectDsl<{}>: {0}", type_name::<T>())]
BadDeser(anyhow::Error),
#[error("This error never happens")]
_Ignonre(PhantomData<fn(T)>, Infallible),
}
impl<T> fmt::Debug for ReflectDslError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ReflectDslError::{BadDeser, BadField, NotRegistered, _Ignonre};
match self {
BadField(field) => f.debug_tuple("BadField").field(field).finish(),
NotRegistered { path, missing } => f
.debug_struct("NotRegistered")
.field("path", path)
.field("missing", missing)
.finish(),
BadDeser(error) => f.debug_tuple("BadDeser").field(error).finish(),
_Ignonre(..) => unreachable!(),
}
}
}

/// A `serde` deserializer used to parse some `input` into a `Box<dyn Reflect>`.
///
/// This is used in [`ReflectDsl`] to deserialize method arguments (`T`'s field values).
/// A default implementation is provided with [`RonFormat`].
pub trait Format {
/// Deserialize into a `Box<dyn Reflect>`, any error is propagated by [`ReflectDsl::method`].
#[allow(clippy::missing_errors_doc)] // false+: We can't say what our users will fail with.
fn deserialize(input: &str, de: TypedReflectDeserializer) -> Result<Box<dyn Reflect>>;
}
/// Deserialize method arguments as `ron` strings.
///
/// This is the default deserialization method for [`ReflectDsl`].
pub struct RonFormat;
impl Format for RonFormat {
fn deserialize(input: &str, de: TypedReflectDeserializer) -> Result<Box<dyn Reflect>> {
Ok(de.deserialize(&mut ron::de::Deserializer::from_str(input)?)?)
}
}

/// Automatic [`ParseDsl`] implementation for any [`Bundle`] + [`Reflect`] `struct`.
///
/// If you find using the `parse_dsl_impl` macro burdensome, and just want to
/// use any bevy `Bundle` as a DSL, you can use `ReflectDsl` to use the `struct`
/// fields as DSL "methods", and [`Reflect` deserialization][refl-deser] to parse
/// the arguments automatically.
///
/// # How to use
///
/// You have a type as follow:
/// ```
/// use cuicui_chirp::ReflectDsl;
/// # use bevy::prelude::*;
///
/// #[derive(Bundle, Reflect, Default)]
/// struct MyBundle {
/// transform: Transform,
/// visibility: Visibility,
/// }
/// # let mut app = App::new();
/// // and you did register it with:
/// app.register_type::<MyBundle>();
/// ```
/// You want to use it in a DSL when parsing files. Consider the following
/// `chirp` file:
/// ```text
/// entity(row) {
/// entity (
/// transform (
/// translation: (x: 0.0, y: 0.0, z: 0.0),
/// rotation: (0.0, 0.0, 0.0, 1.0),
/// scale: (x: 1.0, y: 1.0, z: 1.0),
/// ),
/// visibility Inherited,
/// );
/// }
/// ```
/// You want both `LayoutDsl` methods and fields of `MyBundle` to work in your
/// chirp files.
///
/// In order to do that, you should add the loader for the correct DSL as follow:
/// ```
/// use cuicui_chirp::ReflectDsl;
/// # use bevy::prelude::*;
///
/// # type LayoutDsl = ();
/// # #[derive(Bundle, Reflect, Default)] struct MyBundle { transform: Transform, visibility: Visibility }
/// // Add `ReflectDsl<MyBundle, _>` as an extension on the `LayoutDsl` DSL.
/// type Dsl = ReflectDsl<MyBundle, LayoutDsl>;
/// # let mut app = App::new();
/// app.add_plugins((
/// # bevy::asset::AssetPlugin::default(),
/// // ...
/// // The loader now recognizes the methods `transform` and `visibility`.
/// // They were fields of `MyBundle`
/// cuicui_chirp::loader::Plugin::new::<Dsl>(),
/// ));
/// ```
///
/// # Caveats
///
/// This doesn't work with the `dsl!` macro. You can only use `ReflectDsl` with
/// scenes defined in `chirp` files.
///
/// [refl-deser]: https://docs.rs/bevy_reflect/latest/bevy_reflect/#serialization
#[derive(Debug, Clone, Deref, DerefMut)]
pub struct ReflectDsl<T: Struct, D: DslBundle = (), F: Format = RonFormat> {
inner: Option<T>,
#[deref]
delegate_dsl: D,
_format: PhantomData<F>,
}

impl<T: Default + Struct, D: DslBundle, F: Format> Default for ReflectDsl<T, D, F> {
fn default() -> Self {
Self {
inner: Some(T::default()),
delegate_dsl: D::default(),
_format: PhantomData,
}
}
}
impl<T, D, F> DslBundle for ReflectDsl<T, D, F>
where
T: Bundle + Default + Struct,
D: DslBundle,
F: Format,
{
fn insert(&mut self, cmds: &mut cuicui_dsl::EntityCommands) -> Entity {
// unwrap: This `Self::default` in `Some` state, and only becomes `None` when `insert`
// is called. Since it is only called once, it is fine to unwrap.
cmds.insert(self.inner.take().unwrap());
self.delegate_dsl.insert(cmds)
}
}
impl<T, D, F> ReflectDsl<T, D, F>
where
T: Bundle + Default + Struct,
D: DslBundle,
F: Format,
{
/// This is just so the error type is easier to convert in the `ParseDsl::method` impl.
#[allow(clippy::needless_pass_by_value)] // we'd like to call it as-is
fn typed_method(&mut self, ctx: MethodCtx) -> Result<(), ReflectDslError<T>> {
use ReflectDslError::{BadDeser, BadField};
// unwrap: Same logic as in `DslBundle::insert`
let inner = self.inner.as_mut().unwrap();
let Some(field_to_update) = inner.field_mut(ctx.name) else {
return Err(BadField(ctx.name.to_string()));
};
let id = field_to_update.type_id();
let not_registered = || ReflectDslError::NotRegistered {
path: ctx.name.to_string(),
missing: field_to_update.type_name().to_string(),
};
let registration = ctx.registry.get(id).ok_or_else(not_registered)?;
let de = TypedReflectDeserializer::new(registration, ctx.registry);
let field_value = F::deserialize(ctx.args, de).map_err(BadDeser)?;
// unwrap: Error should never happen, since we get the registration for field.
field_to_update.set(field_value).unwrap();
Ok(())
}
}
impl<T, D, F> ParseDsl for ReflectDsl<T, D, F>
where
T: Bundle + Default + Struct,
D: DslBundle,
F: Format,
{
fn method(&mut self, ctx: MethodCtx) -> Result<()> {
Ok(self.typed_method(ctx)?)
}
}
2 changes: 1 addition & 1 deletion chirp_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ pub(crate) fn parse_dsl_impl(config: &mut ImplConfig, block: &mut syn::ItemImpl)
use #this_crate::wrapparg::{from_str, from_reflect, to_handle, identity};

let MethodCtx { name, args, ctx, registry } = data;
match name.as_ref() {
match name {
#(#funs)*
_name => { #catchall }
}
Expand Down

0 comments on commit 7062948

Please sign in to comment.