Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External dependency directory #236

Merged
merged 3 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/docs/integration-other.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ There are two ways to have explicitly externally-defined types in cddl-codegen:

### Import pathing

In order to make imports easier it's recommended to make a directory corresponding to the dependency and put the `_CDDL_CODEGEN_RAW_BYTES_TYPE_` and `_CDDL_CODEGEN_EXTERN_TYPE_` external types inside of there and then later delete the output directories containing those modules. For an example see the `cml_chain` directory inside of the [`specs/multiera`](https://github.com/dcSpark/cardano-multiplatform-lib/tree/develop/specs/multiera).
If your input directory includes a `/_CDDL_CODEGEN_EXTERN_DEPS_DIR_/` directory, everything inside will be treated as an external dependency. This allows users to specify the import tree of any dependency CDDL structures.
You can define these types as `_CDDL_CODEGEN_EXTERN_TYPE_` if it is entirely self-contained or `_CDDL_CODEGEN_RAW_BYTES_TYPE_` if it is CBOR bytes. For an example see the `_CDDL_CODEGEN_EXTERN_DEPS_DIR_` directory inside of the [`specs/multiera`](https://github.com/dcSpark/cardano-multiplatform-lib/tree/develop/specs/multiera). Each folder within the directory will be treated as a separate dependency. Nothing will be generated by any definitions inside this folder. You will still need to specify the dependency inside the `Cargo.toml` file afterwards.

### Non-black-box types

Another important detail, demonstrated in the above `multiera` CDDL spec, is that when using external types that aren't 100% self-contained (i.e. can't be treated as a black box that implements `Serialize` + `Deserialize`, nor as CBOR bytes implementing `RawBytesEncoding`) like `uint` aliases should be explicitly defined and then removed afterwards. Using the above directory/pathing tip makes this trivial to remove after.
Another important detail, demonstrated in the above `multiera` CDDL spec, is that when using external types that aren't 100% self-contained (i.e. can't be treated as a black box that implements `Serialize` + `Deserialize`, nor as CBOR bytes implementing `RawBytesEncoding`) like `uint` aliases should be explicitly defined and then removed afterwards. Use the above directory/pathing tip.
84 changes: 50 additions & 34 deletions src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,8 @@
/// * {x = Some(}{<value>}{);} - variable assignment (could be nested in function call, etc, too)
/// * {}{<value>}{} - for last-expression eval in blocks
/// * etc
/// We also keep track of if it expects a result and can adjust the generated code based on that

Check failure on line 427 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

doc list item missing indentation
/// to avoid warnings (e.g. avoid Ok(foo?) and directly do foo instead)

Check failure on line 428 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

doc list item missing indentation
struct DeserializeBeforeAfter<'a> {
before: &'a str,
after: &'a str,
Expand Down Expand Up @@ -532,7 +532,7 @@
FixedValue::Text(s) => ("String", format!("\"{s}\".to_owned()")),
};
self.wasm(types, ident)
.new_fn(&convert_to_snake_case(ident.as_ref()))

Check failure on line 535 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
.attr("wasm_bindgen")
.vis("pub")
.ret(ty)
Expand Down Expand Up @@ -809,7 +809,13 @@
.collect::<Vec<_>>();
for scope in scope_names
.iter()
.filter_map(|s| s.components().first())
.filter_map(|s| {
if s.export() {
s.components().first()
} else {
None
}
})
.collect::<BTreeSet<_>>()
{
self.rust_lib().raw(&format!("pub mod {scope};"));
Expand Down Expand Up @@ -895,7 +901,7 @@
for (import_scope, idents) in scope_imports.iter() {
let import_scope = if *import_scope == *ROOT_SCOPE {
Cow::from("crate")
} else if *scope == *ROOT_SCOPE {
} else if *scope == *ROOT_SCOPE || !import_scope.export() {
Cow::from(import_scope.to_string())
} else {
Cow::from(format!("crate::{import_scope}"))
Expand Down Expand Up @@ -974,15 +980,7 @@
// declare submodules
// we do this after the rest to avoid declaring serialization mod/cbor encodings/etc
// for these modules when they only exist to support modules nested deeper
for scope in scope_names.iter() {
let comps = scope.components();
for i in 1..comps.len() {
self.rust_scopes
.entry(ModuleScope::new(comps.as_slice()[0..i].to_vec()))
.or_insert(codegen::Scope::new())
.raw(&format!("pub mod {};", comps[i]));
}
}
declare_modules(&mut self.rust_scopes, &scope_names);

// wasm
if cli.wasm {
Expand All @@ -998,7 +996,13 @@
.collect::<Vec<_>>();
for scope in wasm_scope_names
.iter()
.filter_map(|s| s.components().first())
.filter_map(|s| {
if s.export() {
s.components().first()
} else {
None
}
})
.collect::<BTreeSet<_>>()
{
self.wasm_lib().raw(&format!("pub mod {scope};"));
Expand Down Expand Up @@ -1039,15 +1043,7 @@
// declare submodules
// we do this after the rest to avoid declaring serialization mod/cbor encodings/etc
// for these modules when they only exist to support modules nested deeper
for scope in wasm_scope_names.iter() {
let comps = scope.components();
for i in 1..comps.len() {
self.wasm_scopes
.entry(ModuleScope::new(comps.as_slice()[0..i].to_vec()))
.or_insert(codegen::Scope::new())
.raw(&format!("pub mod {};", comps[i]));
}
}
declare_modules(&mut self.wasm_scopes, &wasm_scope_names);
}
}

Expand Down Expand Up @@ -1099,8 +1095,9 @@
std::fs::create_dir_all(&src_dir)?;
for (scope, content) in other_scopes {
if *scope == *ROOT_SCOPE {
assert!(scope.export());
merged_scope.append(&content.clone());
} else {
} else if scope.export() {
let mod_dir = scope
.components()
.iter()
Expand Down Expand Up @@ -1167,18 +1164,20 @@
// cbor_encodings.rs / {module}/cbor_encodings.rs (if input is a directory)
if cli.preserve_encodings {
for (scope, contents) in self.cbor_encodings_scopes.iter() {
let path = if *scope == *ROOT_SCOPE {
Cow::from("rust/src/cbor_encodings.rs")
} else {
Cow::from(format!(
"rust/src/{}/cbor_encodings.rs",
scope.components().join("/")
))
};
std::fs::write(
rust_dir.join(path.as_ref()),
rustfmt_generated_string(&contents.to_string())?.as_ref(),
)?;
if scope.export() {
let path = if *scope == *ROOT_SCOPE {
Cow::from("rust/src/cbor_encodings.rs")
} else {
Cow::from(format!(
"rust/src/{}/cbor_encodings.rs",
scope.components().join("/")
))
};
std::fs::write(
rust_dir.join(path.as_ref()),
rustfmt_generated_string(&contents.to_string())?.as_ref(),
)?;
}
}
}

Expand Down Expand Up @@ -2442,7 +2441,7 @@
));
}
}
type_check.after(&before_after.after_str(false));

Check failure on line 2444 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
deser_code.content.push_block(type_check);
deser_code.throws = true;
}
Expand Down Expand Up @@ -2783,7 +2782,7 @@
} else {
none_block.line("None");
}
deser_block.after(&before_after.after_str(false));

Check failure on line 2785 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
deser_block.push_block(none_block);
deser_code.content.push_block(deser_block);
deser_code.throws = true;
Expand Down Expand Up @@ -3346,7 +3345,7 @@
new_func.vis("pub");
let can_fail = variant.rust_type().needs_bounds_check_if_inlined(types);
if !variant.rust_type().is_fixed_value() {
new_func.arg(&variant_arg, &variant.rust_type().for_wasm_param(types));

Check failure on line 3348 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
}
let ctor = if variant.rust_type().is_fixed_value() {
format!(
Expand Down Expand Up @@ -3412,7 +3411,7 @@
.s_impl
.new_fn("get")
.vis("pub")
.ret(&element_type.for_wasm_return(types))

Check failure on line 3414 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
.arg_ref_self()
.arg("index", "usize")
.line(element_type.to_wasm_boundary(types, "self.0[index]", false));
Expand Down Expand Up @@ -3644,6 +3643,23 @@
)
}

fn declare_modules(
gen_scopes: &mut BTreeMap<ModuleScope, codegen::Scope>,
module_scopes: &Vec<ModuleScope>,

Check failure on line 3648 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

writing `&Vec` instead of `&[_]` involves a new object where a slice will do
) {
for module_scope in module_scopes.iter() {
if module_scope.export() {
let components = module_scope.components();
for i in 1..components.len() {

Check failure on line 3653 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the loop variable `i` is used to index `components`
gen_scopes
.entry(module_scope.parents(i))
.or_insert(codegen::Scope::new())
.raw(&format!("pub mod {};", components[i]));
}
}
}
}

#[derive(Debug, Clone)]
enum BlockOrLine {
Line(String),
Expand Down Expand Up @@ -5153,7 +5169,7 @@
let mut setter = codegen::Function::new(&format!("set_{}", field.name));
setter
.arg_mut_self()
.arg(&field.name, &field.rust_type.for_wasm_param(types))

Check failure on line 5172 in src/generation.rs

View workflow job for this annotation

GitHub Actions / Rust project

the borrowed expression implements the required traits
.vis("pub");
// don't call needs_bounds_check_if_inlined() since if it's a RustType it's checked during that ctor
if let Some(bounds) = field.rust_type.config.bounds.as_ref() {
Expand Down
34 changes: 26 additions & 8 deletions src/intermediate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,47 @@ use once_cell::sync::Lazy;
pub static ROOT_SCOPE: Lazy<ModuleScope> = Lazy::new(|| vec![String::from("lib")].into());

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct ModuleScope(Vec<String>);
pub struct ModuleScope {
export: bool,
scope: Vec<String>,
}

impl ModuleScope {
pub fn new(scope: Vec<String>) -> Self {
Self::from(scope)
}

/// Make a new ModuleScope using only the first [depth] components
pub fn parents(&self, depth: usize) -> Self {
Self {
export: self.export,
scope: self.scope.as_slice()[0..depth].to_vec(),
}
}

pub fn export(&self) -> bool {
self.export
}

pub fn components(&self) -> &Vec<String> {
&self.0
&self.scope
}
}

impl From<Vec<String>> for ModuleScope {
fn from(scope: Vec<String>) -> Self {
Self(scope)
fn from(mut scope: Vec<String>) -> Self {
let export = match scope.first() {
Some(first_scope) => first_scope != crate::parsing::EXTERN_DEPS_DIR,
None => true,
};
let scope = if export { scope } else { scope.split_off(1) };
Self { export, scope }
}
}

impl std::fmt::Display for ModuleScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.join("::"))
write!(f, "{}", self.scope.join("::"))
}
}

Expand Down Expand Up @@ -87,6 +107,7 @@ pub struct IntermediateTypes<'a> {
generic_instances: BTreeMap<RustIdent, GenericInstance>,
news_can_fail: BTreeSet<RustIdent>,
used_as_key: BTreeSet<RustIdent>,
// which scope an ident is declared in
scopes: BTreeMap<RustIdent, ModuleScope>,
// for scope() to work we keep this here.
// Returning a reference to the const ROOT_SCOPE complains of returning a temporary
Expand Down Expand Up @@ -986,8 +1007,6 @@ mod idents {
// except for defining new cddl rules, since those should not be reserved identifiers
pub fn new(cddl_ident: CDDLIdent) -> Self {
// int is special here since it refers to our own rust struct, not a primitive
println!("{}", cddl_ident.0);

assert!(
!STD_TYPES.contains(&&super::convert_to_camel_case(&cddl_ident.0)[..]),
"Cannot use reserved Rust type name: \"{}\"",
Expand Down Expand Up @@ -1502,7 +1521,6 @@ impl ConceptualRustType {
}

pub fn directly_wasm_exposable(&self, types: &IntermediateTypes) -> bool {
println!("{self:?}.directly_wasm_exposable()");
match self {
Self::Fixed(_) => false,
Self::Primitive(_) => true,
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
components.pop();
}
}
ModuleScope::new(components)
components.join("::")
} else {
ROOT_SCOPE.clone()
ROOT_SCOPE.to_string()
};
std::fs::read_to_string(input_file).map(|raw| {
format!(
Expand Down
1 change: 1 addition & 0 deletions src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum ControlOperator {
}

pub const SCOPE_MARKER: &str = "_CDDL_CODEGEN_SCOPE_MARKER_";
pub const EXTERN_DEPS_DIR: &str = "_CDDL_CODEGEN_EXTERN_DEPS_DIR_";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not easily have a test for this one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could but we'd have to pull in a dependency. I guess we could have a simple crate somewhere in the tests folder and use that and add it to the Cargo.toml of the test after generation as a local dependency. I'll try doing that.

Copy link
Collaborator Author

@rooooooooob rooooooooob May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test. It was a bit more work than I expected. It ended up also testing --common-import-override necessarily so that's a plus at least. I also fixed the failing tests (was only happening on github). Turns out after running cargo from a clean install would cause cddl to update to 0.9.4 (locally it was still using 0.9.1. as specified for me still) which broke basic group support parsing, at least in one of the ways it was used in some of the tests. I made an issue for it anweiss/cddl#222 and set it to =0.9.1 to get around that.

pub const EXTERN_MARKER: &str = "_CDDL_CODEGEN_EXTERN_TYPE_";
pub const RAW_BYTES_MARKER: &str = "_CDDL_CODEGEN_RAW_BYTES_TYPE_";

Expand Down
Loading