Skip to content

Commit

Permalink
Extern generics (#226)
Browse files Browse the repository at this point in the history
Extern generics

Allows types defined by `_CDDL_CODEGEN_EXTERN_TYPE_` to be used with
generic arguments.

Generics will be used directly on the rust side, but due to wasm-bindgen
restrictions must be specified for each concrete type like with current
generic support.
  • Loading branch information
rooooooooob authored Apr 10, 2024
1 parent fb5e489 commit aafb28c
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/docs/comment_dsl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ This can also be useful when you have a spec that is either very awkward to use

This can also be useful when you have a spec that is either very awkward to use (so you hand-write or hand-modify after generation) in some type so you don't generate those types and instead manually merge those hand-written/hand-modified structs back in to the code afterwards. This saves you from having to manually remove all code that is generated regarding `Foo` first before merging in your own.

This also works with generics e.g. you can refer to `foo<T>`. As with other generics this will create a `pub type FooT = Foo<T>;` definition in rust to work with wasm-bindgen's restrictions (no generics) as on the wasm side there will be references to a `FooT` in wasm. The wasm type definition is not emitted as that will be implementation-dependent. For an example see `extern_generic` in the `core` unit test.

## _CDDL_CODEGEN_RAW_BYTES_TYPE_

Expand Down
99 changes: 86 additions & 13 deletions src/intermediate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,13 @@ impl<'a> IntermediateTypes<'a> {

/// mostly for convenience since this is checked in so many places
pub fn is_enum(&self, ident: &RustIdent) -> bool {
matches!(
self.rust_struct(ident).unwrap().variant(),
RustStructType::CStyleEnum { .. }
)
if let Some(rs) = self.rust_struct(ident) {
matches!(rs.variant(), RustStructType::CStyleEnum { .. })
} else {
// could be a generic instead
assert!(self.generic_instances.contains_key(ident));
false
}
}

// this is called by register_table_type / register_array_type automatically
Expand Down Expand Up @@ -518,10 +521,29 @@ impl<'a> IntermediateTypes<'a> {
let resolved_generics = self
.generic_instances
.values()
.map(|instance| instance.resolve(self))
.map(|instance| instance.resolve(self, cli))
.collect::<Vec<_>>();
for resolved_instance in resolved_generics {
self.register_rust_struct(parent_visitor, resolved_instance, cli);
match resolved_instance {
GenericResolved::Resolved(rs) => self.register_rust_struct(parent_visitor, rs, cli),
GenericResolved::Extern {
instance_ident,
real_ident,
} => {
// must be generic extern - register it so other lookups don't fail
self.register_rust_struct(
parent_visitor,
RustStruct::new_extern(instance_ident.clone()),
cli,
);
// we do direct rust alias replacing (gen_rust_alias=false) since no problems with generics in rust
// but wasm_bindgen can't work with it directly we assume the user will supply the correct mappings
self.register_type_alias(
instance_ident,
AliasInfo::new(ConceptualRustType::Rust(real_ident).into(), true, false),
);
}
}
}
// recursively check all types used as keys or contained within a type used as a key
// this is so we only derive comparison or hash traits for those types
Expand Down Expand Up @@ -902,7 +924,9 @@ impl Primitive {
}

mod idents {
use crate::{rust_reserved::STD_TYPES, utils::is_identifier_reserved};
use crate::{cli::Cli, rust_reserved::STD_TYPES, utils::is_identifier_reserved};

use super::{IntermediateTypes, RustType};

// to resolve ambiguities between raw (from CDDL) and already-formatted
// for things like type aliases, etc, we use these wrapper structs
Expand Down Expand Up @@ -961,6 +985,23 @@ mod idents {

Self(super::convert_to_camel_case(&cddl_ident.0))
}

pub fn new_generic(
generic_ident: &RustIdent,
generic_args: &[RustType],
types: &IntermediateTypes,
cli: &Cli,
) -> Self {
Self(format!(
"{}<{}>",
generic_ident,
generic_args
.iter()
.map(|a| a.for_rust_member(types, false, cli))
.collect::<Vec<String>>()
.join(", ")
))
}
}

impl std::fmt::Display for RustIdent {
Expand Down Expand Up @@ -2655,6 +2696,19 @@ pub struct GenericInstance {
generic_args: Vec<RustType>,
}

#[derive(Debug, Clone)]
pub enum GenericResolved {
// resolved with types swapped to concrete instance
Resolved(RustStruct),
// could not resolve (def is extern)
Extern {
// internal generic ident e.g. FooBar for Foo<Bar>
instance_ident: RustIdent,
// actual data type e.g. Foo<Bar>
real_ident: RustIdent,
},
}

impl GenericInstance {
pub fn new(
instance_ident: RustIdent,
Expand All @@ -2670,13 +2724,32 @@ impl GenericInstance {

// TODO: should we rename fields / variant names after-the-fact?
// (for the cases where the name came from the original generic param)
pub fn resolve(&self, types: &IntermediateTypes) -> RustStruct {
// returns None when it can't be resolved i.e. extern defs
// this will be left to the user instead to handle.
pub fn resolve(&self, types: &IntermediateTypes, cli: &Cli) -> GenericResolved {
let def = match types.generic_defs.get(&self.generic_ident) {
Some(def) => def,
None => panic!(
"Generic instance used on {} without definition",
self.generic_ident
),
None => {
if types
.rust_struct(&self.generic_ident)
.map(|rs| matches!(rs.variant(), RustStructType::Extern))
.unwrap_or(false)
{
return GenericResolved::Extern {
instance_ident: self.instance_ident.clone(),
real_ident: RustIdent::new_generic(
&self.generic_ident,
&self.generic_args,
types,
cli,
),
};
}
panic!(
"Generic instance used on {} without definition | {:?}",
self.generic_ident, self
);
}
};
assert_eq!(def.generic_params.len(), self.generic_args.len());
let resolved_args = def
Expand Down Expand Up @@ -2726,7 +2799,7 @@ impl GenericInstance {
panic!("generics not supported on raw bytes types")
}
};
instance
GenericResolved::Resolved(instance)
}

fn resolve_type(args: &BTreeMap<&RustIdent, &RustType>, orig: &RustType) -> RustType {
Expand Down
6 changes: 6 additions & 0 deletions tests/core/input.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ externs = {
? opt: external_foo,
}

extern_generic = _CDDL_CODEGEN_EXTERN_TYPE_

using_extern_generic = [
foo: extern_generic<external_foo>,
]

; types below test codegen_table_type

standalone_table = { * uint => text }
Expand Down
7 changes: 7 additions & 0 deletions tests/core/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@ mod tests {
deser_test(&externs);
}

#[test]
fn externs_generic() {
deser_test(&UsingExternGeneric::new(
ExternGeneric::new(ExternalFoo::new(u64::MAX, String::from("asdfghjkl"), vec![0])),
));
}

#[test]
fn top_level_arrays() {
// this part of the test just tests that the resulting code compiles
Expand Down
25 changes: 25 additions & 0 deletions tests/external_rust_defs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,28 @@ impl serialization::Deserialize for ExternalFoo {
.map_err(|e| e.annotate("ExternalFoo"))
}
}

#[derive(Clone, Debug)]
pub struct ExternGeneric<T>(pub T);

impl<T> ExternGeneric<T> {
pub fn new(x: T) -> Self {
Self(x)
}
}

impl<T: cbor_event::se::Serialize> cbor_event::se::Serialize for ExternGeneric<T> {
fn serialize<'se, W: std::io::Write>(
&self,
serializer: &'se mut cbor_event::se::Serializer<W>,
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>> {
self.0.serialize(serializer)
}
}

impl<T: serialization::Deserialize> serialization::Deserialize for ExternGeneric<T> {
fn deserialize<R: std::io::BufRead + std::io::Seek>(raw: &mut cbor_event::de::Deserializer<R>) -> Result<Self, error::DeserializeError> {
T::deserialize(raw).map(Self)
}
}

24 changes: 24 additions & 0 deletions tests/external_rust_defs_compiles_with_json_preserve
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,27 @@ impl serialization::Deserialize for ExternalFoo {
.map_err(|e| e.annotate("ExternalFoo"))
}
}

#[derive(Clone, Debug)]
pub struct ExternGeneric<T>(pub T);

impl<T> ExternGeneric<T> {
pub fn new(x: T) -> Self {
Self(x)
}
}

impl<T: cbor_event::se::Serialize> cbor_event::se::Serialize for ExternGeneric<T> {
fn serialize<'se, W: std::io::Write>(
&self,
serializer: &'se mut cbor_event::se::Serializer<W>,
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>> {
self.0.serialize(serializer)
}
}

impl<T: serialization::Deserialize> serialization::Deserialize for ExternGeneric<T> {
fn deserialize<R: std::io::BufRead + std::io::Seek>(raw: &mut cbor_event::de::Deserializer<R>) -> Result<Self, error::DeserializeError> {
T::deserialize(raw).map(Self)
}
}
14 changes: 14 additions & 0 deletions tests/external_wasm_defs
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,17 @@ impl AsRef<cddl_lib::ExternalFoo> for ExternalFoo {
&self.0
}
}

type ExternGenericExternalFoo = ExternalFoo;

impl From<cddl_lib::ExternGeneric<cddl_lib::ExternalFoo>> for ExternalFoo {
fn from(native: cddl_lib::ExternGeneric<cddl_lib::ExternalFoo>) -> Self {
native.0.into()
}
}

impl From<ExternalFoo> for cddl_lib::ExternGeneric<cddl_lib::ExternalFoo> {
fn from(wasm: ExternalFoo) -> Self {
cddl_lib::ExternGeneric::new(wasm.0)
}
}

0 comments on commit aafb28c

Please sign in to comment.