From dccc0a419a76c3afc7671f232812ddc2a930c9d2 Mon Sep 17 00:00:00 2001 From: Stuart Harris Date: Fri, 22 Nov 2024 11:14:07 +0000 Subject: [PATCH] tidy and readme --- crux_cli/src/codegen/README.md | 157 ++++++++++++++++++ crux_cli/src/codegen/generator.rs | 15 +- crux_cli/src/codegen/parser.rs | 92 +++++----- crux_http/CHANGELOG.md | 2 +- crux_kv/CHANGELOG.md | 2 +- crux_time/CHANGELOG.md | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- 7 files changed, 211 insertions(+), 63 deletions(-) create mode 100644 crux_cli/src/codegen/README.md diff --git a/crux_cli/src/codegen/README.md b/crux_cli/src/codegen/README.md new file mode 100644 index 000000000..6472fdda7 --- /dev/null +++ b/crux_cli/src/codegen/README.md @@ -0,0 +1,157 @@ +# Crux CLI codegen for foreign types + +The codegen command on the `crux` CLI generates code for foreign types in Swift, +Kotlin and TypeScript. + +> [!NOTE] This is a work in progress and is not yet ready for general use. + +```sh +crux codegen --lib shared +``` + +The `--lib` flag specifies the library to generate code for. The `shared` +library is used in this example. + +## How it works + +### Source data + +Generate `rustdoc` JSON for the library, using the +[`rustdoc_json`][rustdocJsonReference] crate. We want to use the latest format +version so we currently require Rust nightly to be installed +(`rustup install nightly`). This JSON describes all the public and private items +in the library and is deserialized using the +[`rustdoc-types`][rustdocTypesReference] crate. + +We currently only look into the specified library and not its dependencies, but +this will change. + +### Build a graph + +Parse the JSON and build a graph of the relationships that we are interested in. + +Start by adding edges for any `impl` blocks that implement the `App` or `Effect` +traits. + +Then add edges for the `Event`, `ViewModel`, and `Capabilities` associated types +in the `App` impl. + +Then add edges for struct fields and enum variants. + +```mermaid +graph TD + Ty([Type]) + S([Struct]) + SF([Struct Field]) + E([Enum]) + V([Variant]) + I([Impl]) + AI([Associated Item]) + TA([App Trait]) + TE([Effect Trait]) + S --> |Field| SF + E --> |Variant| V + V --> |Field| SF + SF --> |Type| Ty + I --> |AssociatedItem| AI + AI --> |AssociatedType| Ty + I -.-> |TraitApp| TA + I -.-> |TraitEffect| TE + + +``` + +### Process the graph + +Process the data using the [`ascent`][ascentCrateReference] crate to run a logic +program (similar to Datalog) on the graph. + +```rust +ascent! { + relation edge(Node, Node, Edge); + + relation app(Node); + relation effect(Node); + relation is_effect_of_app(Node, Node); + relation root(Node); + relation parent(Node, Node); + relation field(Node, Node); + relation variant(Node, Node); + + // app structs have an implementation of the App trait + app(app) <-- + edge(app_impl, app_trait, Edge::TraitApp), + edge(app_impl, app, Edge::Type); + + // effect enums have an implementation of the Effect trait + effect(effect) <-- + edge(effect_impl, effect_trait, Edge::TraitEffect), + edge(effect_impl, effect, Edge::Type); + + // an effect belongs to an app if they are in the same module + is_effect_of_app(app, effect) <-- + app(app), + effect(effect), + if are_in_same_module(app, effect); + + // Event and ViewModel types are associated + // with the root apps (that have no parent) + root(assoc_type) <-- + edge(app_impl, app_trait, Edge::TraitApp), + edge(app_impl, app, Edge::Type), + !parent(_, app), + edge(app_impl, assoc_item, Edge::AssociatedItem), + edge(assoc_item, assoc_type, Edge::AssociatedType); + // Effects belong to the root apps (that have no parent) + root(effect_enum) <-- + is_effect_of_app(app, effect_enum), + !parent(_, app); + + // app hierarchy + parent(parent, child) <-- + app(parent), + app(child), + edge(parent, field, Edge::Field), + edge(field, child, Edge::Type); + + // fields of root structs + field(struct_, field) <-- + root(struct_), + edge(struct_, field, ?Edge::Variant|Edge::Field); + // recursive descent + field(struct2, field2) <-- + field(struct1, field1), + edge(field1, struct2, Edge::Type), + edge(struct2, field2, ?Edge::Variant|Edge::Field); + + // variants of root enums + variant(enum_, variant) <-- + root(enum_), + edge(enum_, variant, Edge::Variant); + // recursive descent + variant(variant, field) <-- + variant(enum_, variant), + edge(variant, field, Edge::Field); +} +``` + +### Create an intermediate representation + +For now we are using the same IR as the +[`serde_generate`][serdeGenerateReference] crate that we currently use for +typegen. This gives us a backend for free and should allow us to maintain +backwards compatibility. + +### Generate foreign types + +The IR is used to generate code for foreign types in Swift, Kotlin and +TypeScript, via a vendored version of the +[`serde_generate`][serdeGenerateReference] crate. + +We will likely want to change this in the future to allow us to generate more +idiomatic code for each language, and support Crux more fully. + +[ascentCrateReference]: https://crates.io/crates/ascent +[rustdocJsonReference]: https://crates.io/crates/rustdoc-json +[rustdocTypesReference]: https://crates.io/crates/rustdoc-types +[serdeGenerateReference]: https://crates.io/crates/serde-generate diff --git a/crux_cli/src/codegen/generator.rs b/crux_cli/src/codegen/generator.rs index 767b98dbf..24c59e2f0 100644 --- a/crux_cli/src/codegen/generator.rs +++ b/crux_cli/src/codegen/generator.rs @@ -22,8 +22,8 @@ pub(crate) fn generate(edges: &[(Node, Node)], crate_: &Crate) { let mut container = None; match &item.inner { rustdoc_types::ItemEnum::Struct(s) => match &s.kind { - rustdoc_types::StructKind::Unit => {} - rustdoc_types::StructKind::Tuple(_vec) => {} + rustdoc_types::StructKind::Unit => (), + rustdoc_types::StructKind::Tuple(_vec) => (), rustdoc_types::StructKind::Plain { fields: _, has_stripped_fields: _, @@ -213,15 +213,8 @@ fn get_name(node: &Node, qualified: bool) -> Option { fn qualify_name(item_summary: Option<&ItemSummary>, name: &str) -> String { item_summary .map(|p| { - p.path - .iter() - .rev() - .skip(1) - .rev() - .chain([name.to_string()].iter()) - .map(|s| s.as_str()) - .collect::>() - .as_slice() + [&p.path[..(p.path.len() - 1)], &[name.to_string()]] + .concat() .join("::") }) .unwrap_or(name.to_string()) diff --git a/crux_cli/src/codegen/parser.rs b/crux_cli/src/codegen/parser.rs index 313e5f03d..95695c5e0 100644 --- a/crux_cli/src/codegen/parser.rs +++ b/crux_cli/src/codegen/parser.rs @@ -9,8 +9,10 @@ use rustdoc_types::{ use serde::Serialize; ascent! { + // input data relation edge(Node, Node, Edge); + // result data relation app(Node); relation effect(Node); relation is_effect_of_app(Node, Node); @@ -19,29 +21,31 @@ ascent! { relation field(Node, Node); relation variant(Node, Node); - // app structs + // app structs have an implementation of the App trait app(app) <-- - edge(app_impl, app_trait, Edge::OfTraitApp), - edge(app_impl, app, Edge::ForType); + edge(app_impl, app_trait, Edge::TraitApp), + edge(app_impl, app, Edge::Type); - // effect enums + // effect enums have an implementation of the Effect trait effect(effect) <-- - edge(effect_impl, effect_trait, Edge::OfTraitEffect), - edge(effect_impl, effect, Edge::ForType); + edge(effect_impl, effect_trait, Edge::TraitEffect), + edge(effect_impl, effect, Edge::Type); + // an effect belongs to an app if they are in the same module is_effect_of_app(app, effect) <-- app(app), effect(effect), if are_in_same_module(app, effect); - // root for Event and ViewModel + // Event and ViewModel types are associated + // with the root apps (that have no parent) root(assoc_type) <-- - edge(app_impl, app_trait, Edge::OfTraitApp), - edge(app_impl, app, Edge::ForType), + edge(app_impl, app_trait, Edge::TraitApp), + edge(app_impl, app, Edge::Type), !parent(_, app), edge(app_impl, assoc_item, Edge::AssociatedItem), edge(assoc_item, assoc_type, Edge::AssociatedType); - // root for Effect + // Effects belong to the root apps (that have no parent) root(effect_enum) <-- is_effect_of_app(app, effect_enum), !parent(_, app); @@ -50,23 +54,27 @@ ascent! { parent(parent, child) <-- app(parent), app(child), - edge(parent, field, Edge::HasField), - edge(field, child, Edge::ForType); + edge(parent, field, Edge::Field), + edge(field, child, Edge::Type); + // fields of root structs field(struct_, field) <-- root(struct_), - edge(struct_, field, ?Edge::HasVariant|Edge::HasField|Edge::Unit); + edge(struct_, field, ?Edge::Variant|Edge::Field); + // recursive descent field(struct2, field2) <-- field(struct1, field1), - edge(field1, struct2, Edge::ForType), - edge(struct2, field2, ?Edge::HasVariant|Edge::HasField|Edge::Unit); + edge(field1, struct2, Edge::Type), + edge(struct2, field2, ?Edge::Variant|Edge::Field); + // variants of root enums variant(enum_, variant) <-- root(enum_), - edge(enum_, variant, Edge::HasVariant); + edge(enum_, variant, Edge::Variant); + // recursive descent variant(variant, field) <-- variant(enum_, variant), - edge(variant, field, Edge::HasField); + edge(variant, field, Edge::Field); } fn are_in_same_module(app: &Node, effect: &Node) -> bool { @@ -126,17 +134,14 @@ pub fn parse(crate_: &Crate) -> Result> { ItemEnum::Union(_union) => (), ItemEnum::Struct(s) => { match &s.kind { - StructKind::Unit => { - prog.edge.push((source.clone(), source.clone(), Edge::Unit)); - } + StructKind::Unit => (), StructKind::Tuple(fields) => { for field in fields { if let Some(id) = field { let Some(dest) = node_by_id(id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::HasField)); + prog.edge.push((source.clone(), dest.clone(), Edge::Field)); } } } @@ -148,8 +153,7 @@ pub fn parse(crate_: &Crate) -> Result> { let Some(dest) = node_by_id(id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::HasField)); + prog.edge.push((source.clone(), dest.clone(), Edge::Field)); } } }; @@ -159,8 +163,7 @@ pub fn parse(crate_: &Crate) -> Result> { let Some(dest) = node_by_id(&path.id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::ForType)); + prog.edge.push((source.clone(), dest.clone(), Edge::Type)); if let Some(args) = &path.args { process_args(source, args.as_ref(), &node_by_id, &mut prog); @@ -174,7 +177,7 @@ pub fn parse(crate_: &Crate) -> Result> { continue; }; prog.edge - .push((source.clone(), dest.clone(), Edge::HasVariant)); + .push((source.clone(), dest.clone(), Edge::Variant)); } } ItemEnum::Variant(v) => { @@ -186,8 +189,7 @@ pub fn parse(crate_: &Crate) -> Result> { let Some(dest) = node_by_id(id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::HasField)); + prog.edge.push((source.clone(), dest.clone(), Edge::Field)); } } } @@ -199,8 +201,7 @@ pub fn parse(crate_: &Crate) -> Result> { let Some(dest) = node_by_id(id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::HasField)); + prog.edge.push((source.clone(), dest.clone(), Edge::Field)); } } }; @@ -223,8 +224,8 @@ pub fn parse(crate_: &Crate) -> Result> { .. }) => { let trait_edge = match trait_name.as_str() { - "App" => Edge::OfTraitApp, - "Effect" => Edge::OfTraitEffect, + "App" => Edge::TraitApp, + "Effect" => Edge::TraitEffect, _ => continue, }; @@ -238,8 +239,7 @@ pub fn parse(crate_: &Crate) -> Result> { let Some(dest) = node_by_id(&for_type_id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::ForType)); + prog.edge.push((source.clone(), dest.clone(), Edge::Type)); // record edges for the associated items in the impl for id in items { @@ -325,11 +325,11 @@ pub fn parse(crate_: &Crate) -> Result> { serde_json::to_string(&prog.parent).unwrap(), )?; - let mut all_edges = Vec::new(); - all_edges.extend(prog.field); - all_edges.extend(prog.variant); + let mut all = Vec::new(); + all.extend(prog.field); + all.extend(prog.variant); - Ok(all_edges) + Ok(all) } fn process_args<'a>( @@ -345,8 +345,7 @@ fn process_args<'a>( let Some(dest) = node_by_id(&path.id) else { continue; }; - prog.edge - .push((source.clone(), dest.clone(), Edge::ForType)); + prog.edge.push((source.clone(), dest.clone(), Edge::Type)); if let Some(args) = &path.args { let generic_args = args.as_ref(); @@ -385,10 +384,9 @@ impl std::hash::Hash for Node { enum Edge { AssociatedItem, AssociatedType, - ForType, - HasField, - HasVariant, - OfTraitApp, - OfTraitEffect, - Unit, + Type, + Field, + Variant, + TraitApp, + TraitEffect, } diff --git a/crux_http/CHANGELOG.md b/crux_http/CHANGELOG.md index 838c325b8..35c9520ef 100644 --- a/crux_http/CHANGELOG.md +++ b/crux_http/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to ### Other -- tidy and docs udpate +- tidy and docs update - update http and kv tests to use new API ## [0.10.2](https://github.com/redbadger/crux/compare/crux_http-v0.10.1...crux_http-v0.10.2) - 2024-20-21 diff --git a/crux_kv/CHANGELOG.md b/crux_kv/CHANGELOG.md index 34ca1d7e5..300ed7392 100644 --- a/crux_kv/CHANGELOG.md +++ b/crux_kv/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to ### Other -- tidy and docs udpate +- tidy and docs update - update http and kv tests to use new API ## [0.5.1](https://github.com/redbadger/crux/compare/crux_kv-v0.5.0...crux_kv-v0.5.1) - 2024-20-21 diff --git a/crux_time/CHANGELOG.md b/crux_time/CHANGELOG.md index f85688cbe..c654e09e5 100644 --- a/crux_time/CHANGELOG.md +++ b/crux_time/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to ### Other -- tidy and docs udpate +- tidy and docs update - remove unused test event ## [0.5.1](https://github.com/redbadger/crux/compare/crux_time-v0.5.0...crux_time-v0.5.1) - 2024-10-21 diff --git a/templates/simple_counter/Android/gradle/wrapper/gradle-wrapper.properties b/templates/simple_counter/Android/gradle/wrapper/gradle-wrapper.properties index 05ad2da45..e0c18369d 100644 --- a/templates/simple_counter/Android/gradle/wrapper/gradle-wrapper.properties +++ b/templates/simple_counter/Android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed May 24 10:42:32 BST 2023 +#Fri Nov 22 09:22:18 GMT 2024 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME