From 75a75dde7350ab3c040b172590f6d73e9df03a46 Mon Sep 17 00:00:00 2001 From: "remy.baranx@gmail.com" Date: Thu, 12 Sep 2024 09:15:55 +0200 Subject: [PATCH] feat: dispatcher_from_tag! macro --- crates/dojo-lang/src/compiler.rs | 44 ++++++++- crates/dojo-lang/src/contract.rs | 83 ++++++++++++++++- .../src/inline_macros/dispatcher_from_tag.rs | 93 +++++++++++++++++++ crates/dojo-lang/src/inline_macros/mod.rs | 1 + crates/dojo-lang/src/inline_macros/utils.rs | 16 ++++ crates/dojo-lang/src/interface.rs | 11 ++- crates/dojo-lang/src/plugin.rs | 22 ++++- .../semantics/test_data/dispatcher_from_tag | 66 +++++++++++++ crates/dojo-lang/src/semantics/tests.rs | 2 + crates/dojo-world/src/manifest/types.rs | 1 + .../dojo_examples-actions-40b6994c.toml | 1 + .../dojo_examples-dungeon-6620e0e6.toml | 1 + .../dojo_examples-mock_token-31599eb2.toml | 1 + .../dojo_examples-others-61de2c18.toml | 1 + examples/spawn-and-move/src/actions.cairo | 3 +- 15 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 crates/dojo-lang/src/inline_macros/dispatcher_from_tag.rs create mode 100644 crates/dojo-lang/src/semantics/test_data/dispatcher_from_tag diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index b1f1e7c6c2..b1cf524b17 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -40,7 +40,7 @@ use starknet::core::types::contract::SierraClass; use starknet::core::types::Felt; use tracing::{debug, trace, trace_span}; -use crate::plugin::{DojoAuxData, Model}; +use crate::plugin::{DojoAuxData, Model, Trait}; use crate::scarb_internal::debug::SierraToCairoDebugInfo; #[derive(Debug, Clone)] @@ -350,6 +350,7 @@ fn update_files( let mut models = BTreeMap::new(); let mut contracts = BTreeMap::new(); + let mut dojo_interfaces = vec![]; if let Some(external_contracts) = external_contracts { let external_crate_ids = collect_external_crate_ids(db, external_contracts); @@ -378,6 +379,7 @@ fn update_files( &naming::get_tag(&contract.namespace, &contract.name), &compiled_artifacts, &contract.systems, + &contract.traits, )?); } @@ -392,6 +394,9 @@ fn update_files( *module_id, &compiled_artifacts, )?); + + // update the list of Dojo interface + dojo_interfaces.extend(dojo_aux_data.interfaces.iter().map(|i| i.name.clone())); } // StarknetAuxData shouldn't be required. Every dojo contract and model are starknet @@ -422,7 +427,37 @@ fn update_files( std::fs::create_dir_all(&base_contracts_abis_dir)?; } - for (_, (manifest, module_id, artifact)) in contracts.iter_mut() { + for (qualified_path, (manifest, module_id, artifact, traits)) in contracts.iter_mut() { + // During compilation, a list of traits implemented inside the contract is extracted. + // We need to find which of these traits is a Dojo interface, to be able to store its qualified path, + // and be able to use it in macros such `dispatcher_from_tag!` + let found_interfaces = + traits.iter().filter(|t| dojo_interfaces.contains(&t.name)).collect::>(); + + // a contract may or may not implement a Dojo interface, but it cannot implement several Dojo interfaces. + if found_interfaces.len() > 1 { + return Err(anyhow!( + "The contract '{}' cannot implement several Dojo interfaces (found: [{}]).", + manifest.inner.tag, + found_interfaces.iter().map(|t| t.name.clone()).collect::>().join(", ") + )); + } + + manifest.inner.interface_path = + found_interfaces.first().map_or(String::new(), |x| x.path.clone()); + + // the Dojo interface path may start with `super`, referencing the trait which is + // in the same file than the contract. + // In this case, just replace `super` by the contract qualified path. + if manifest.inner.interface_path.starts_with("super") { + let (path, _) = qualified_path + .rsplit_once(CAIRO_PATH_SEPARATOR) + .unwrap_or((qualified_path.as_str(), "")); + + manifest.inner.interface_path = + manifest.inner.interface_path.replacen("super", &path, 1); + } + write_manifest_and_abi( &base_contracts_dir, &base_contracts_abis_dir, @@ -558,7 +593,8 @@ fn get_dojo_contract_artifacts( tag: &str, compiled_classes: &CompiledArtifactByPath, systems: &[String], -) -> Result, ModuleId, CompiledArtifact)>> { + traits: &Vec, +) -> Result, ModuleId, CompiledArtifact, Vec)>> { let mut result = HashMap::new(); if !matches!(naming::get_name_from_tag(tag).as_str(), "world" | "resource_metadata" | "base") { @@ -584,7 +620,7 @@ fn get_dojo_contract_artifacts( result.insert( contract_qualified_path.to_string(), - (manifest, *module_id, artifact.clone()), + (manifest, *module_id, artifact.clone(), traits.clone()), ); } } diff --git a/crates/dojo-lang/src/contract.rs b/crates/dojo-lang/src/contract.rs index 23810b113b..f69b47f436 100644 --- a/crates/dojo-lang/src/contract.rs +++ b/crates/dojo-lang/src/contract.rs @@ -18,7 +18,7 @@ use dojo_types::system::Dependency; use dojo_world::config::NamespaceConfig; use dojo_world::contracts::naming; -use crate::plugin::{ContractAuxData, DojoAuxData, DOJO_CONTRACT_ATTR}; +use crate::plugin::{ContractAuxData, DojoAuxData, Trait, DOJO_CONTRACT_ATTR}; use crate::syntax::world_param::{self, WorldParamInjectionKind}; use crate::syntax::{self_param, utils as syntax_utils}; @@ -50,6 +50,7 @@ impl DojoContract { let mut diagnostics = vec![]; let parameters = get_parameters(db, module_ast, &mut diagnostics); + let traits = get_traits(db, module_ast); let mut contract = DojoContract { diagnostics, dependencies: HashMap::new(), systems: vec![] }; @@ -262,8 +263,10 @@ impl DojoContract { namespace: contract_namespace.clone(), dependencies: contract.dependencies.values().cloned().collect(), systems: contract.systems.clone(), + traits, }], events: vec![], + interfaces: vec![], })), code_mappings, }), @@ -732,3 +735,81 @@ fn get_parameters( parameters } + +fn get_traits(db: &dyn SyntaxGroup, module_ast: &ast::ItemModule) -> Vec { + let traits = if let ast::MaybeModuleBody::Some(body) = module_ast.body(db) { + body.items(db) + .elements(db) + .iter() + .filter_map(|e| { + if let ast::ModuleItem::Impl(x) = e { + let mut path_segments = x.trait_path(db).elements(db); + + // in Cairo, there is always a trait path linked to an impl, so path_segments always contain an item + let last_segment = path_segments.pop().unwrap(); + + // a dojo interface always has a generic argument, so just keep this kind of traits. + match last_segment { + ast::PathSegment::WithGenericArgs(p) => { + let trait_name = p.ident(db).text(db).to_string(); + + // Here, we have to rebuild the full trait path. There are several cases: + // 1) there is no path with the trait name (example: IActions) + // => find the trait path in `use` clauses. + // 2) the path is relative (example: `players::IActions`) + // => not possible to be sure that there is only one path in `use` clauses that matches + // with this relative path. + // (example: 2 use clauses: `use path1::players` and `use path2::players`). + // 3) the path is absolute (example: `path::to::players::IActions`) + // => just use this path. + // + // At the moment, only cases 1) and 3) are supported. + let trait_path = if path_segments.is_empty() { + get_trait_path(db, &body, trait_name.clone()) + .expect("a path must always be found") + } else { + format!( + "{}::{trait_name}", + path_segments + .iter() + .map(|p| p.as_syntax_node().get_text(db)) + .collect::>() + .join("::") + ) + }; + + Some(Trait { name: trait_name.clone(), path: trait_path }) + } + _ => None, + } + } else { + None + } + }) + .collect::>() + } else { + vec![] + }; + + traits +} + +fn get_trait_path( + db: &dyn SyntaxGroup, + module_body: &ast::ModuleBody, + trait_name: String, +) -> Option { + for e in module_body.items(db).elements(db) { + if let ast::ModuleItem::Use(u) = e { + if let ast::UsePath::Single(s) = u.use_path(db) { + let trait_path = s.as_syntax_node().get_text(db); + + if trait_path.ends_with(&trait_name) { + return Some(trait_path); + } + } + } + } + + None +} diff --git a/crates/dojo-lang/src/inline_macros/dispatcher_from_tag.rs b/crates/dojo-lang/src/inline_macros/dispatcher_from_tag.rs new file mode 100644 index 0000000000..e63dafde29 --- /dev/null +++ b/crates/dojo-lang/src/inline_macros/dispatcher_from_tag.rs @@ -0,0 +1,93 @@ +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_defs::plugin::{ + InlineMacroExprPlugin, InlinePluginResult, MacroPluginMetadata, NamedPlugin, PluginDiagnostic, + PluginGeneratedFile, +}; +use cairo_lang_defs::plugin_utils::unsupported_bracket_diagnostic; +use cairo_lang_diagnostics::Severity; +use cairo_lang_syntax::node::{ast, TypedStablePtr, TypedSyntaxNode}; +use dojo_world::contracts::naming; + +use super::utils::find_interface_path; + +#[derive(Debug, Default)] +pub struct DispatcherFromTagMacro; + +impl NamedPlugin for DispatcherFromTagMacro { + const NAME: &'static str = "dispatcher_from_tag"; +} + +impl InlineMacroExprPlugin for DispatcherFromTagMacro { + fn generate_code( + &self, + db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, + syntax: &ast::ExprInlineMacro, + metadata: &MacroPluginMetadata<'_>, + ) -> InlinePluginResult { + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = syntax.arguments(db) else { + return unsupported_bracket_diagnostic(db, syntax); + }; + + let args = arg_list.arguments(db).elements(db); + + if args.len() != 2 { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "Invalid arguments. Expected dispatcher_from_tag!(\"tag\", contract_address)" + .to_string(), + severity: Severity::Error, + }], + }; + } + + let tag = &args[0].as_syntax_node().get_text(db).replace('\"', ""); + let contract_address = args[1].as_syntax_node().get_text(db); + + if !naming::is_valid_tag(tag) { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "Invalid tag. Tag must be in the format of `namespace-name`." + .to_string(), + severity: Severity::Error, + }], + }; + } + + // read the interface path from the manifest and generate a dispatcher: + // Dispatcher { contract_address }; + let interface_path = match find_interface_path(metadata.cfg_set, tag) { + Ok(interface_path) => interface_path, + Err(_e) => { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: format!("Failed to find the interface path of `{tag}`"), + severity: Severity::Error, + }], + }; + } + }; + + let mut builder = PatchBuilder::new(db, syntax); + builder.add_str(&format!( + "{interface_path}Dispatcher {{ contract_address: {contract_address}}}", + )); + + let (code, code_mappings) = builder.build(); + + InlinePluginResult { + code: Some(PluginGeneratedFile { + name: "dispatcher_from_tag_macro".into(), + content: code, + code_mappings, + aux_data: None, + }), + diagnostics: vec![], + } + } +} diff --git a/crates/dojo-lang/src/inline_macros/mod.rs b/crates/dojo-lang/src/inline_macros/mod.rs index 5c90d35004..2b41de498f 100644 --- a/crates/dojo-lang/src/inline_macros/mod.rs +++ b/crates/dojo-lang/src/inline_macros/mod.rs @@ -5,6 +5,7 @@ use cairo_lang_syntax::node::{ast, Terminal, TypedStablePtr, TypedSyntaxNode}; use smol_str::SmolStr; pub mod delete; +pub mod dispatcher_from_tag; pub mod emit; pub mod get; pub mod get_models_test_class_hashes; diff --git a/crates/dojo-lang/src/inline_macros/utils.rs b/crates/dojo-lang/src/inline_macros/utils.rs index 53bb8342fa..c5064fb6d2 100644 --- a/crates/dojo-lang/src/inline_macros/utils.rs +++ b/crates/dojo-lang/src/inline_macros/utils.rs @@ -33,6 +33,22 @@ pub fn parent_of_kind( None } +/// +pub fn find_interface_path(cfg_set: &CfgSet, contract_tag: &str) -> anyhow::Result { + let dojo_manifests_dir = get_dojo_manifests_dir(cfg_set.clone())?; + + let base_dir = dojo_manifests_dir.join("base"); + let base_manifest = BaseManifest::load_from_path(&base_dir)?; + + for contract in base_manifest.contracts { + if contract.inner.tag == contract_tag { + return Ok(contract.inner.interface_path); + } + } + + Err(anyhow::anyhow!("Unable to find the interface path of `{}`", contract_tag)) +} + /// Reads all the models and namespaces from base manifests files. pub fn load_manifest_models_and_namespaces( cfg_set: &CfgSet, diff --git a/crates/dojo-lang/src/interface.rs b/crates/dojo-lang/src/interface.rs index 68bdee54bb..d41e7285fb 100644 --- a/crates/dojo-lang/src/interface.rs +++ b/crates/dojo-lang/src/interface.rs @@ -1,6 +1,7 @@ use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; use cairo_lang_defs::plugin::{ - MacroPluginMetadata, PluginDiagnostic, PluginGeneratedFile, PluginResult, + DynGeneratedFileAuxData, MacroPluginMetadata, PluginDiagnostic, PluginGeneratedFile, + PluginResult, }; use cairo_lang_diagnostics::Severity; use cairo_lang_plugins::plugins::HasItemsInCfgEx; @@ -8,6 +9,7 @@ use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, ids, Terminal, TypedStablePtr, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; +use crate::plugin::{DojoAuxData, InterfaceAuxData}; use crate::syntax::self_param; use crate::syntax::world_param::{self, WorldParamInjectionKind}; @@ -84,7 +86,12 @@ impl DojoInterface { code: Some(PluginGeneratedFile { name: name.clone(), content: code, - aux_data: None, + aux_data: Some(DynGeneratedFileAuxData::new(DojoAuxData { + models: vec![], + contracts: vec![], + events: vec![], + interfaces: vec![InterfaceAuxData { name: name.to_string() }], + })), code_mappings, }), diagnostics: interface.diagnostics, diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 317bf2cb87..caf37279b5 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -28,6 +28,7 @@ use url::Url; use crate::contract::DojoContract; use crate::event::handle_event_struct; use crate::inline_macros::delete::DeleteMacro; +use crate::inline_macros::dispatcher_from_tag::DispatcherFromTagMacro; use crate::inline_macros::emit::EmitMacro; use crate::inline_macros::get::GetMacro; use crate::inline_macros::get_models_test_class_hashes::GetModelsTestClassHashes; @@ -54,12 +55,24 @@ pub struct Model { pub members: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Trait { + pub name: String, + pub path: String, +} + #[derive(Debug, PartialEq, Eq)] pub struct ContractAuxData { pub name: SmolStr, pub namespace: String, pub dependencies: Vec, pub systems: Vec, + pub traits: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InterfaceAuxData { + pub name: String, } /// Dojo related auxiliary data of the Dojo plugin. @@ -71,6 +84,8 @@ pub struct DojoAuxData { pub contracts: Vec, /// A list of events that were processed by the plugin. pub events: Vec, + /// A list of interfaces that were processed by the plugin. + pub interfaces: Vec, } impl GeneratedFileAuxData for DojoAuxData { @@ -78,7 +93,11 @@ impl GeneratedFileAuxData for DojoAuxData { self } fn eq(&self, other: &dyn GeneratedFileAuxData) -> bool { - if let Some(other) = other.as_any().downcast_ref::() { self == other } else { false } + if let Some(other) = other.as_any().downcast_ref::() { + self == other + } else { + false + } } } @@ -158,6 +177,7 @@ pub fn dojo_plugin_suite() -> PluginSuite { .add_inline_macro_plugin::() .add_inline_macro_plugin::() .add_inline_macro_plugin::() + .add_inline_macro_plugin::() .add_inline_macro_plugin::() .add_inline_macro_plugin::(); diff --git a/crates/dojo-lang/src/semantics/test_data/dispatcher_from_tag b/crates/dojo-lang/src/semantics/test_data/dispatcher_from_tag new file mode 100644 index 0000000000..e86935709c --- /dev/null +++ b/crates/dojo-lang/src/semantics/test_data/dispatcher_from_tag @@ -0,0 +1,66 @@ +//! > Test no param + +//! > test_runner_name +test_semantics + +//! > expression +dispatcher_from_tag!() + +//! > expected +Missing( + ExprMissing { + ty: , + }, +) + +//! > semantic_diagnostics +error: Plugin diagnostic: Invalid arguments. Expected dispatcher_from_tag!("tag", contract_address) + --> lib.cairo:2:1 +dispatcher_from_tag!() +^********************^ + +//! > ========================================================================== + +//! > Test missing address + +//! > test_runner_name +test_semantics + +//! > expression +dispatcher_from_tag!("dojo-foo_setter") + +//! > expected +Missing( + ExprMissing { + ty: , + }, +) + +//! > semantic_diagnostics +error: Plugin diagnostic: Invalid arguments. Expected dispatcher_from_tag!("tag", contract_address) + --> lib.cairo:2:1 +dispatcher_from_tag!("dojo-foo_setter") +^*************************************^ + +//! > ========================================================================== + +//! > Test ok but expected to fail due to missing dojo_manifests_dir + +//! > test_runner_name +test_semantics + +//! > expression +dispatcher_from_tag!("dojo-foo_setter", 0x1234) + +//! > expected +Missing( + ExprMissing { + ty: , + }, +) + +//! > semantic_diagnostics +error: Plugin diagnostic: Failed to find the interface path of `dojo-foo_setter` + --> lib.cairo:2:1 +dispatcher_from_tag!("dojo-foo_setter", 0x1234) +^*********************************************^ diff --git a/crates/dojo-lang/src/semantics/tests.rs b/crates/dojo-lang/src/semantics/tests.rs index 2e8fba8a44..5169ad2608 100644 --- a/crates/dojo-lang/src/semantics/tests.rs +++ b/crates/dojo-lang/src/semantics/tests.rs @@ -19,6 +19,8 @@ test_file_test!( selector_from_tag: "selector_from_tag", + dispatcher_from_tag: "dispatcher_from_tag", + get_models_test_class_hashes: "get_models_test_class_hashes", spawn_test_world: "spawn_test_world", diff --git a/crates/dojo-world/src/manifest/types.rs b/crates/dojo-world/src/manifest/types.rs index 49f6b9163e..a1dc414861 100644 --- a/crates/dojo-world/src/manifest/types.rs +++ b/crates/dojo-world/src/manifest/types.rs @@ -107,6 +107,7 @@ pub struct DojoContract { pub init_calldata: Vec, pub tag: String, pub systems: Vec, + pub interface_path: String, } /// Represents a declaration of a model. diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml index 0ae312f5c4..b7c4eabdfe 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-actions-40b6994c.toml @@ -18,4 +18,5 @@ systems = [ "update_player_name", "update_player_items", ] +interface_path = "dojo_examples::actions::IActions" manifest_name = "dojo_examples-actions-40b6994c" diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-dungeon-6620e0e6.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-dungeon-6620e0e6.toml index 75d087c886..d44349769e 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-dungeon-6620e0e6.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-dungeon-6620e0e6.toml @@ -8,4 +8,5 @@ writes = [] init_calldata = [] tag = "dojo_examples-dungeon" systems = ["enter"] +interface_path = "dojo_examples::dungeon::IDungeon" manifest_name = "dojo_examples-dungeon-6620e0e6" diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-mock_token-31599eb2.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-mock_token-31599eb2.toml index 6385a30c6c..d3640472f8 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-mock_token-31599eb2.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-mock_token-31599eb2.toml @@ -8,4 +8,5 @@ writes = [] init_calldata = [] tag = "dojo_examples-mock_token" systems = [] +interface_path = "" manifest_name = "dojo_examples-mock_token-31599eb2" diff --git a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-others-61de2c18.toml b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-others-61de2c18.toml index 5a43d3fad8..d41ec08f38 100644 --- a/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-others-61de2c18.toml +++ b/examples/spawn-and-move/manifests/dev/base/contracts/dojo_examples-others-61de2c18.toml @@ -8,4 +8,5 @@ writes = [] init_calldata = [] tag = "dojo_examples-others" systems = [] +interface_path = "" manifest_name = "dojo_examples-others-61de2c18" diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 70c6c5c16e..b9c48c6b29 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -166,7 +166,8 @@ pub mod actions { let river_skale = RiverSkale { id: 1, health: 5, armor: 3, attack: 2 }; set!(world, (flatbow, river_skale)); - IDungeonDispatcher { contract_address: dungeon_address }.enter(); + let d = dispatcher_from_tag("dojo_examples-dungeon", dungeon_address); + d.enter(); } fn update_player_name(ref world: IWorldDispatcher, name: ByteArray) {