diff --git a/Cargo.lock b/Cargo.lock index c07a009fa9..5787d9e7f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2805,6 +2805,16 @@ dependencies = [ "linkme", ] +[[package]] +name = "cairo-lang-macro" +version = "0.1.1" +source = "git+https://github.com/software-mansion/scarb?rev=aff99810c37ceb77b61b3bd2ecee14a253a3397e#aff99810c37ceb77b61b3bd2ecee14a253a3397e" +dependencies = [ + "cairo-lang-macro-attributes", + "cairo-lang-macro-stable", + "linkme", +] + [[package]] name = "cairo-lang-macro-attributes" version = "0.1.0" @@ -4807,6 +4817,24 @@ dependencies = [ "dojo-lang 1.0.1", ] +[[package]] +name = "dojo-macros" +version = "0.1.0" +dependencies = [ + "cairo-lang-defs", + "cairo-lang-macro 0.1.1", + "cairo-lang-parser", + "cairo-lang-plugins", + "cairo-lang-syntax", + "cairo-lang-utils", + "convert_case 0.6.0", + "dojo-types 1.0.1", + "serde", + "serde_json", + "starknet 0.12.0", + "starknet-crypto 0.7.2", +] + [[package]] name = "dojo-metrics" version = "1.0.1" @@ -12777,7 +12805,7 @@ dependencies = [ "cairo-lang-filesystem", "cairo-lang-formatter", "cairo-lang-lowering", - "cairo-lang-macro", + "cairo-lang-macro 0.1.0", "cairo-lang-macro-stable", "cairo-lang-parser", "cairo-lang-semantic", @@ -13671,7 +13699,6 @@ dependencies = [ "clap-verbosity-flag", "colored", "dojo-bindgen", - "dojo-lang 1.0.1", "dojo-test-utils", "dojo-types 1.0.1", "dojo-utils", diff --git a/Cargo.toml b/Cargo.toml index 43968de1b8..0cdbf4fe3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ members = [ "bin/torii", "crates/dojo/bindgen", "crates/dojo/core", +# TODO: to be removed but still used by some tools like LS "crates/dojo/lang", + "crates/dojo/macros", "crates/dojo/test-utils", "crates/dojo/types", "crates/dojo/utils", @@ -77,7 +79,9 @@ dojo-metrics = { path = "crates/metrics" } # dojo-lang dojo-bindgen = { path = "crates/dojo/bindgen" } dojo-core = { path = "crates/dojo/core" } +# TODO: to be removed dojo-lang = { path = "crates/dojo/lang" } +dojo-macros = { path = "crates/dojo/macros" } dojo-test-utils = { path = "crates/dojo/test-utils" } dojo-types = { path = "crates/dojo/types" } dojo-world = { path = "crates/dojo/world" } diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 6831fbf059..d200b5073a 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -23,7 +23,6 @@ clap.workspace = true clap-verbosity-flag.workspace = true colored.workspace = true dojo-bindgen.workspace = true -dojo-lang.workspace = true dojo-types.workspace = true dojo-utils.workspace = true dojo-world.workspace = true diff --git a/bin/sozo/src/commands/test.rs b/bin/sozo/src/commands/test.rs index 772a87df1a..6c18f7f213 100644 --- a/bin/sozo/src/commands/test.rs +++ b/bin/sozo/src/commands/test.rs @@ -16,7 +16,6 @@ use cairo_lang_test_plugin::{test_plugin_suite, TestsCompilationConfig}; use cairo_lang_test_runner::{CompiledTestRunner, RunProfilerConfig, TestCompiler, TestRunConfig}; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; use clap::Args; -use dojo_lang::dojo_plugin_suite; use itertools::Itertools; use scarb::compiler::{ CairoCompilationUnit, CompilationUnit, CompilationUnitAttributes, ContractSelector, @@ -197,7 +196,6 @@ pub(crate) fn build_root_database(unit: &CairoCompilationUnit) -> Result { } // dojo decorator -#[dojo::contract] +#[dojo_contract] mod actions { use super::IActions; diff --git a/crates/dojo/core-cairo-test/Scarb.lock b/crates/dojo/core-cairo-test/Scarb.lock index 2c1a7ab14c..2a5bcf8ff2 100644 --- a/crates/dojo/core-cairo-test/Scarb.lock +++ b/crates/dojo/core-cairo-test/Scarb.lock @@ -3,9 +3,9 @@ version = 1 [[package]] name = "dojo" -version = "1.0.0-rc.0" +version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] @@ -13,8 +13,9 @@ name = "dojo_cairo_test" version = "1.0.0-rc.0" dependencies = [ "dojo", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/crates/dojo/core-cairo-test/Scarb.toml b/crates/dojo/core-cairo-test/Scarb.toml index b7e2111a73..e119fe16a8 100644 --- a/crates/dojo/core-cairo-test/Scarb.toml +++ b/crates/dojo/core-cairo-test/Scarb.toml @@ -11,5 +11,6 @@ dojo = { path = "../core" } [dev-dependencies] cairo_test = "=2.8.4" +dojo_macros = { path = "../macros" } [lib] diff --git a/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo b/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo index e76b91b479..fa196f9b6b 100644 --- a/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo +++ b/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo @@ -20,7 +20,7 @@ use crate::world::{spawn_test_world, NamespaceDef, TestResource}; use crate::utils::GasCounterTrait; #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] struct CaseNotPacked { #[key] pub owner: ContractAddress, @@ -29,7 +29,7 @@ struct CaseNotPacked { } #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] struct ComplexModel { #[key] pub game_id: u128, @@ -205,7 +205,7 @@ fn bench_simple_struct() { } #[derive(Copy, Drop, Serde, IntrospectPacked)] -#[dojo::model] +#[dojo_model] struct PositionWithQuaterions { #[key] id: felt252, diff --git a/crates/dojo/core-cairo-test/src/tests/contract.cairo b/crates/dojo/core-cairo-test/src/tests/contract.cairo index 1fc1539eb1..d42cca95d0 100644 --- a/crates/dojo/core-cairo-test/src/tests/contract.cairo +++ b/crates/dojo/core-cairo-test/src/tests/contract.cairo @@ -23,7 +23,7 @@ pub mod contract_invalid_upgrade { } } -#[dojo::contract] +#[dojo_contract] mod test_contract {} #[starknet::interface] diff --git a/crates/dojo/core-cairo-test/src/tests/event/event.cairo b/crates/dojo/core-cairo-test/src/tests/event/event.cairo index 2753abdcc8..f43cfe4317 100644 --- a/crates/dojo/core-cairo-test/src/tests/event/event.cairo +++ b/crates/dojo/core-cairo-test/src/tests/event/event.cairo @@ -1,5 +1,5 @@ #[derive(Drop, Serde)] -#[dojo::event] +#[dojo_event] struct FooEvent { #[key] k1: u8, diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo index 4b41eaf263..361ace2d24 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo +++ b/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo @@ -19,7 +19,7 @@ struct FooBaseEvent { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct FooEventBadLayoutType { #[key] pub caller: ContractAddress, @@ -28,7 +28,7 @@ pub struct FooEventBadLayoutType { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] struct FooEventMemberRemoved { #[key] pub caller: ContractAddress, @@ -37,7 +37,7 @@ struct FooEventMemberRemoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] struct FooEventMemberAddedButRemoved { #[key] pub caller: ContractAddress, @@ -46,7 +46,7 @@ struct FooEventMemberAddedButRemoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] struct FooEventMemberAddedButMoved { #[key] pub caller: ContractAddress, @@ -55,7 +55,7 @@ struct FooEventMemberAddedButMoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] struct FooEventMemberAdded { #[key] pub caller: ContractAddress, diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo index f0e859ec86..e80ed1989f 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo +++ b/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo @@ -10,7 +10,7 @@ use crate::world::{ pub const DOJO_NSH: felt252 = 0x309e09669bc1fdc1dd6563a7ef862aa6227c97d099d08cc7b81bad58a7443fa; #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct SimpleEvent { #[key] pub id: u32, @@ -18,7 +18,7 @@ pub struct SimpleEvent { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] pub struct Foo { #[key] pub caller: ContractAddress, @@ -27,7 +27,7 @@ pub struct Foo { } #[derive(Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] pub struct NotCopiable { #[key] pub caller: ContractAddress, @@ -87,7 +87,7 @@ pub trait IFooSetter { fn set_foo(ref self: T, a: felt252, b: u128); } -#[dojo::contract] +#[dojo_contract] pub mod foo_setter { use super::{Foo, IFooSetter}; use dojo::model::ModelStorage; @@ -101,10 +101,10 @@ pub mod foo_setter { } } -#[dojo::contract] +#[dojo_contract] pub mod test_contract {} -#[dojo::contract] +#[dojo_contract] pub mod test_contract_with_dojo_init_args { fn dojo_init(ref self: ContractState, arg1: felt252) { let _a = arg1; @@ -118,7 +118,7 @@ pub struct Sword { } #[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Case { #[key] pub owner: ContractAddress, @@ -127,7 +127,7 @@ pub struct Case { } #[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Character { #[key] pub caller: ContractAddress, @@ -173,7 +173,7 @@ pub trait Ibar { fn delete_foo(self: @TContractState); } -#[dojo::contract] +#[dojo_contract] pub mod bar { use core::traits::Into; use starknet::{get_caller_address}; diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo index 238f3e4fe0..51fad32cd4 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo +++ b/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo @@ -10,7 +10,7 @@ use crate::world::{spawn_test_world, NamespaceDef, TestResource}; /// These model contracts are used to test model upgrades in tests/model.cairo. #[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct FooModelBadLayoutType { #[key] pub caller: ContractAddress, @@ -19,7 +19,7 @@ struct FooModelBadLayoutType { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct FooModelMemberRemoved { #[key] pub caller: ContractAddress, @@ -28,7 +28,7 @@ struct FooModelMemberRemoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct FooModelMemberAddedButRemoved { #[key] pub caller: ContractAddress, @@ -37,7 +37,7 @@ struct FooModelMemberAddedButRemoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct FooModelMemberAddedButMoved { #[key] pub caller: ContractAddress, @@ -46,7 +46,7 @@ struct FooModelMemberAddedButMoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct FooModelMemberAdded { #[key] pub caller: ContractAddress, diff --git a/crates/dojo/core-cairo-test/src/tests/model/model.cairo b/crates/dojo/core-cairo-test/src/tests/model/model.cairo index ff48b33192..b77b26512b 100644 --- a/crates/dojo/core-cairo-test/src/tests/model/model.cairo +++ b/crates/dojo/core-cairo-test/src/tests/model/model.cairo @@ -3,7 +3,7 @@ use dojo::world::WorldStorage; use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource}; #[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] struct Foo { #[key] k1: u8, @@ -15,7 +15,7 @@ struct Foo { #[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] struct Foo2 { #[key] k1: u8, diff --git a/crates/dojo/core-cairo-test/src/tests/utils/hash.cairo b/crates/dojo/core-cairo-test/src/tests/utils/hash.cairo index 362cd65035..f2ecbce460 100644 --- a/crates/dojo/core-cairo-test/src/tests/utils/hash.cairo +++ b/crates/dojo/core-cairo-test/src/tests/utils/hash.cairo @@ -4,7 +4,7 @@ use dojo::utils::selector_from_names; use crate::tests::helpers::DOJO_NSH; #[derive(Drop, Copy, Serde)] -#[dojo::model] +#[dojo_model] struct MyModel { #[key] x: u8, diff --git a/crates/dojo/core-cairo-test/src/tests/world/event.cairo b/crates/dojo/core-cairo-test/src/tests/world/event.cairo index 2c52779447..866c0c9794 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/event.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/event.cairo @@ -8,7 +8,7 @@ use dojo::world::{world, IWorldDispatcherTrait}; use dojo::event::Event; #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct FooEventMemberRemoved { #[key] pub caller: ContractAddress, @@ -16,7 +16,7 @@ pub struct FooEventMemberRemoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct FooEventMemberAddedButRemoved { #[key] pub caller: ContractAddress, @@ -26,7 +26,7 @@ pub struct FooEventMemberAddedButRemoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct FooEventMemberAddedButMoved { #[key] pub caller: ContractAddress, @@ -36,7 +36,7 @@ pub struct FooEventMemberAddedButMoved { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] +#[dojo_event] pub struct FooEventMemberAdded { #[key] pub caller: ContractAddress, diff --git a/crates/dojo/core-cairo-test/src/tests/world/model.cairo b/crates/dojo/core-cairo-test/src/tests/world/model.cairo index 64cce8ee3e..623cebf1e3 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/model.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/model.cairo @@ -9,7 +9,7 @@ use dojo::model::Model; #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct FooModelBadLayoutType { #[key] pub caller: ContractAddress, @@ -18,7 +18,7 @@ pub struct FooModelBadLayoutType { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct FooModelMemberRemoved { #[key] pub caller: ContractAddress, @@ -26,7 +26,7 @@ pub struct FooModelMemberRemoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct FooModelMemberAddedButRemoved { #[key] pub caller: ContractAddress, @@ -36,7 +36,7 @@ pub struct FooModelMemberAddedButRemoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct FooModelMemberAddedButMoved { #[key] pub caller: ContractAddress, @@ -46,7 +46,7 @@ pub struct FooModelMemberAddedButMoved { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct FooModelMemberAdded { #[key] pub caller: ContractAddress, diff --git a/crates/dojo/core/Scarb.lock b/crates/dojo/core/Scarb.lock index 5a313fab2b..4b129b0176 100644 --- a/crates/dojo/core/Scarb.lock +++ b/crates/dojo/core/Scarb.lock @@ -5,9 +5,9 @@ version = 1 name = "dojo" version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/crates/dojo/core/Scarb.toml b/crates/dojo/core/Scarb.toml index 52d1390721..0f20f20be5 100644 --- a/crates/dojo/core/Scarb.toml +++ b/crates/dojo/core/Scarb.toml @@ -7,8 +7,7 @@ version = "1.0.1" [dependencies] starknet = "=2.8.4" -dojo_plugin = { path = "../lang" } -#dojo_macros = { path = "../macros" } +dojo_macros = { path = "../macros" } [dev-dependencies] cairo_test = "=2.8.4" diff --git a/crates/dojo/core/src/model/metadata.cairo b/crates/dojo/core/src/model/metadata.cairo index 512d4c14c0..d06e64ec86 100644 --- a/crates/dojo/core/src/model/metadata.cairo +++ b/crates/dojo/core/src/model/metadata.cairo @@ -4,7 +4,7 @@ use dojo::model::model::Model; use dojo::utils; #[derive(Introspect, Drop, Serde, PartialEq, Clone, Debug)] -#[dojo::model] +#[dojo_model] pub struct ResourceMetadata { #[key] pub resource_id: felt252, diff --git a/crates/dojo/lang/src/attribute_macros/contract.rs b/crates/dojo/lang/src/attribute_macros/contract.rs index 2f9c94c9b7..057f7f068b 100644 --- a/crates/dojo/lang/src/attribute_macros/contract.rs +++ b/crates/dojo/lang/src/attribute_macros/contract.rs @@ -35,32 +35,6 @@ impl DojoContract { module_ast: &ast::ItemModule, metadata: &MacroPluginMetadata<'_>, ) -> PluginResult { - let name = module_ast.name(db).text(db); - - let mut contract = DojoContract { diagnostics: vec![], systems: vec![] }; - - for (id, value) in [("name", &name.to_string())] { - if !naming::is_name_valid(value) { - return PluginResult { - code: None, - diagnostics: vec![PluginDiagnostic { - stable_ptr: module_ast.stable_ptr().0, - message: format!( - "The contract {id} '{value}' can only contain characters (a-z/A-Z), \ - digits (0-9) and underscore (_)." - ), - severity: Severity::Error, - }], - remove_original_item: false, - }; - } - } - - let mut has_event = false; - let mut has_storage = false; - let mut has_init = false; - let mut has_constructor = false; - if let MaybeModuleBody::Some(body) = module_ast.body(db) { let mut body_nodes: Vec<_> = body .iter_items_in_cfg(db, metadata.cfg_set) diff --git a/crates/dojo/lang/src/plugin_test_data/event b/crates/dojo/lang/src/plugin_test_data/event index ffd85c4e82..0c019d238a 100644 --- a/crates/dojo/lang/src/plugin_test_data/event +++ b/crates/dojo/lang/src/plugin_test_data/event @@ -8,7 +8,7 @@ event //! > cairo_code #[derive(Drop, Serde)] -#[dojo::event] +#[dojo_event] pub struct Message { #[key] pub identity: ContractAddress, @@ -35,7 +35,7 @@ struct MyEventNoHistorical { //! > expanded_cairo_code #[derive(Drop, Serde)] -#[dojo::event] +#[dojo_event] pub struct Message { #[key] pub identity: ContractAddress, diff --git a/crates/dojo/lang/src/plugin_test_data/model b/crates/dojo/lang/src/plugin_test_data/model index ad5ec36787..23b5ebb3e5 100644 --- a/crates/dojo/lang/src/plugin_test_data/model +++ b/crates/dojo/lang/src/plugin_test_data/model @@ -85,31 +85,31 @@ struct ModelWithStringNamespace { v: Vec3, } -#[dojo::model] +#[dojo_model] struct Position { #[key] id: felt252, v: Vec3, } -#[dojo::model] +#[dojo_model] struct Roles { role_ids: Array } -#[dojo::model] +#[dojo_model] struct OnlyKeyModel { #[key] id: felt252 } -#[dojo::model] +#[dojo_model] struct U256KeyModel { #[key] id: u256 } -#[dojo::model] +#[dojo_model] struct Player { #[key] game: felt252, @@ -118,10 +118,10 @@ struct Player { name: felt252, } -#[dojo::model] +#[dojo_model] type OtherPlayer = Player; -#[dojo::model] +#[dojo_model] struct ModelWithSimpleArray { #[key] player: ContractAddress, @@ -129,7 +129,7 @@ struct ModelWithSimpleArray { y: Array } -#[dojo::model] +#[dojo_model] struct ModelWithByteArray { #[key] player: ContractAddress, @@ -137,7 +137,7 @@ struct ModelWithByteArray { y: ByteArray } -#[dojo::model] +#[dojo_model] struct ModelWithComplexArray { #[key] player: ContractAddress, @@ -145,7 +145,7 @@ struct ModelWithComplexArray { y: Array } -#[dojo::model] +#[dojo_model] struct ModelWithTuple { #[key] player: ContractAddress, @@ -153,7 +153,7 @@ struct ModelWithTuple { y: (u8, u16, u32) } -#[dojo::model] +#[dojo_model] struct ModelWithTupleNoPrimitives { #[key] player: ContractAddress, @@ -240,31 +240,31 @@ struct ModelWithStringNamespace { v: Vec3, } -#[dojo::model] +#[dojo_model] struct Position { #[key] id: felt252, v: Vec3, } -#[dojo::model] +#[dojo_model] struct Roles { role_ids: Array } -#[dojo::model] +#[dojo_model] struct OnlyKeyModel { #[key] id: felt252 } -#[dojo::model] +#[dojo_model] struct U256KeyModel { #[key] id: u256 } -#[dojo::model] +#[dojo_model] struct Player { #[key] game: felt252, @@ -273,10 +273,10 @@ struct Player { name: felt252, } -#[dojo::model] +#[dojo_model] type OtherPlayer = Player; -#[dojo::model] +#[dojo_model] struct ModelWithSimpleArray { #[key] player: ContractAddress, @@ -284,7 +284,7 @@ struct ModelWithSimpleArray { y: Array } -#[dojo::model] +#[dojo_model] struct ModelWithByteArray { #[key] player: ContractAddress, @@ -292,7 +292,7 @@ struct ModelWithByteArray { y: ByteArray } -#[dojo::model] +#[dojo_model] struct ModelWithComplexArray { #[key] player: ContractAddress, @@ -300,7 +300,7 @@ struct ModelWithComplexArray { y: Array } -#[dojo::model] +#[dojo_model] struct ModelWithTuple { #[key] player: ContractAddress, @@ -308,7 +308,7 @@ struct ModelWithTuple { y: (u8, u16, u32) } -#[dojo::model] +#[dojo_model] struct ModelWithTupleNoPrimitives { #[key] player: ContractAddress, @@ -9113,70 +9113,70 @@ error: Expected args. error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:79:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:79:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:103:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:103:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:115:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:115:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:123:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:123:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:131:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:131:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:139:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:139:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:147:1 -#[dojo::model] +#[dojo_model] ^************^ error: Expected args. --> /tmp/plugin_test/model/src/lib.cairo:147:1 -#[dojo::model] +#[dojo_model] ^************^ diff --git a/crates/dojo/lang/src/plugin_test_data/system b/crates/dojo/lang/src/plugin_test_data/system index 004fbe5dc7..68a409643a 100644 --- a/crates/dojo/lang/src/plugin_test_data/system +++ b/crates/dojo/lang/src/plugin_test_data/system @@ -34,7 +34,7 @@ mod proxy { } } -#[dojo::contract] +#[dojo_contract] mod ctxnamed { use traits::Into; use dojo::world::Context; @@ -44,7 +44,7 @@ mod ctxnamed { } } -#[dojo::contract] +#[dojo_contract] mod withevent { #[event] #[derive(Drop, starknet::Event)] @@ -70,7 +70,7 @@ mod testcomponent2 { struct Storage {} } -#[dojo::contract] +#[dojo_contract] mod withcomponent { component!(path: testcomponent1, storage: testcomponent1_storage, event: testcomponent1_event); component!(path: testcomponent2, storage: testcomponent2_storage, event: testcomponent2_event); @@ -130,7 +130,7 @@ trait IFaultyTrait { fn do_with_world_not_first(vec: Vec2, ref world: IWorldDispatcher) -> felt252; } -#[dojo::contract] +#[dojo_contract] mod MyFaultyContract { #[abi(embed_v0)] impl TestFaultyImpl of IFaultyTrait { @@ -177,7 +177,7 @@ mod MyFaultyContract { } } -#[dojo::contract] +#[dojo_contract] mod MyNominalContract { #[derive(Drop)] struct Action { @@ -228,13 +228,13 @@ mod MyNominalContract { } } -#[dojo::contract] +#[dojo_contract] mod constructor_test { #[constructor] fn constructor(ref self: ContractState, _value: u8) {} } -#[dojo::contract] +#[dojo_contract] mod no_init_test {} //! > generated_cairo_code diff --git a/crates/dojo/lang/src/semantics/test_data/set b/crates/dojo/lang/src/semantics/test_data/set index a6e9918296..e1c6876253 100644 --- a/crates/dojo/lang/src/semantics/test_data/set +++ b/crates/dojo/lang/src/semantics/test_data/set @@ -64,7 +64,7 @@ test_semantics use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; #[derive(Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] struct Health { #[key] id: u32, diff --git a/crates/dojo/macros-test/Scarb.lock b/crates/dojo/macros-test/Scarb.lock new file mode 100644 index 0000000000..b4ff837ddb --- /dev/null +++ b/crates/dojo/macros-test/Scarb.lock @@ -0,0 +1,35 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "1.0.1" +dependencies = [ + "dojo_macros", +] + +[[package]] +name = "dojo_macros" +version = "0.1.0" + +[[package]] +name = "macros_test" +version = "0.1.0" +dependencies = [ + "dojo", + "dojo_macros", + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.33.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.33.0#221b1dbff42d650e9855afd4283508da8f8cacba" + +[[package]] +name = "snforge_std" +version = "0.33.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.33.0#221b1dbff42d650e9855afd4283508da8f8cacba" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/crates/dojo/macros-test/Scarb.toml b/crates/dojo/macros-test/Scarb.toml new file mode 100644 index 0000000000..57666a21ae --- /dev/null +++ b/crates/dojo/macros-test/Scarb.toml @@ -0,0 +1,14 @@ +[package] +name = "macros_test" +description = "Testing stand-alone project for proc macros." +version = "0.1.0" +cairo-version = "=2.8.4" +edition = "2024_07" + +[dependencies] +starknet = "=2.8.4" +dojo_macros = { path = "../macros" } +dojo = { path = "../core" } + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.33.0" } diff --git a/crates/dojo/macros-test/src/lib.cairo b/crates/dojo/macros-test/src/lib.cairo new file mode 100644 index 0000000000..fb4e8d4bb4 --- /dev/null +++ b/crates/dojo/macros-test/src/lib.cairo @@ -0,0 +1,37 @@ +#[dojo_model] +#[derive(Drop, Serde, Introspect)] +struct MyModel { + #[key] + k: u128, + v: u128 +} + +#[dojo_event] +#[derive(Drop, Serde, Introspect)] +struct MyEvent { + #[key] + player: u32, + damage: u64 +} + +#[starknet::interface] +trait MyInterface { + fn action1(ref self: T); + fn action2(self: @T); +} + +#[dojo_contract] +mod MyContract { + use super::MyInterface; + + #[abi(embed_v0)] + impl MyInterfaceImpl of MyInterface { + fn action1(ref self: ContractState) {} + fn action2(self: @ContractState) {} + } +} + +#[test] +fn test_one() { + assert!(false); +} diff --git a/crates/dojo/macros/Cargo.toml b/crates/dojo/macros/Cargo.toml new file mode 100644 index 0000000000..44414e93a7 --- /dev/null +++ b/crates/dojo/macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dojo-macros" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +cairo-lang-macro = {git = "https://github.com/software-mansion/scarb", rev="aff99810c37ceb77b61b3bd2ecee14a253a3397e"} +cairo-lang-defs.workspace = true +cairo-lang-parser.workspace = true +cairo-lang-plugins.workspace = true +cairo-lang-syntax.workspace = true +cairo-lang-utils.workspace = true +convert_case.workspace = true +dojo-types.workspace = true +serde.workspace = true +serde_json.workspace = true +starknet.workspace = true +starknet-crypto.workspace = true diff --git a/crates/dojo/macros/Scarb.lock b/crates/dojo/macros/Scarb.lock new file mode 100644 index 0000000000..2e5a11a7a4 --- /dev/null +++ b/crates/dojo/macros/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo_macros" +version = "0.1.0" diff --git a/crates/dojo/macros/Scarb.toml b/crates/dojo/macros/Scarb.toml new file mode 100644 index 0000000000..17a88dd076 --- /dev/null +++ b/crates/dojo/macros/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "dojo_macros" +version = "0.1.0" +description = "Dojo macros" +homepage = "https://github.com/dojoengine/dojo" +edition = "2024_07" + +[cairo-plugin] diff --git a/crates/dojo/macros/src/attributes/constants.rs b/crates/dojo/macros/src/attributes/constants.rs new file mode 100644 index 0000000000..4b9862c151 --- /dev/null +++ b/crates/dojo/macros/src/attributes/constants.rs @@ -0,0 +1,8 @@ +/// Dojo attribute names. +/// Note that, at the moment, these names must match with +/// proc macro function names. +pub const DOJO_CONTRACT_ATTR: &str = "dojo_contract"; +pub const DOJO_EVENT_ATTR: &str = "dojo_event"; +pub const DOJO_MODEL_ATTR: &str = "dojo_model"; + +pub const DOJO_ATTR_NAMES: [&str; 3] = [DOJO_CONTRACT_ATTR, DOJO_EVENT_ATTR, DOJO_MODEL_ATTR]; diff --git a/crates/dojo/macros/src/attributes/dojo_contract.rs b/crates/dojo/macros/src/attributes/dojo_contract.rs new file mode 100644 index 0000000000..5e7212092a --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_contract.rs @@ -0,0 +1,340 @@ +//! `dojo_contract` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostic, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::node::ast::{MaybeModuleBody, OptionReturnTypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::BodyItems; +use cairo_lang_syntax::node::kind::SyntaxKind::ItemModule; +use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use super::constants::DOJO_CONTRACT_ATTR; +use super::struct_parser::{validate_attributes, validate_namings_diagnostics}; +use crate::diagnostic_ext::DiagnosticsExt; + +const CONSTRUCTOR_FN: &str = "constructor"; +const DOJO_INIT_FN: &str = "dojo_init"; + +const CONTRACT_PATCH: &str = include_str!("./patches/contract.patch.cairo"); +const DEFAULT_INIT_PATCH: &str = include_str!("./patches/default_init.patch.cairo"); + +#[attribute_macro] +pub fn dojo_contract(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_module_attribute_macro(token_stream) +} + +pub fn handle_module_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (root_node, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in root_node.descendants(&db) { + // Process only the first module expected to be the contract. + if n.kind(&db) == ItemModule { + let module_ast = ast::ItemModule::from_syntax_node(&db, n); + return from_module(&db, &module_ast); + } + } + + ProcMacroResult::new(TokenStream::empty()) +} + +pub fn from_module(db: &dyn SyntaxGroup, module_ast: &ast::ItemModule) -> ProcMacroResult { + let name = module_ast.name(db).text(db); + + let mut diagnostics = vec![]; + + diagnostics.extend(validate_attributes(db, &module_ast.attributes(db), DOJO_CONTRACT_ATTR)); + + diagnostics.extend(validate_namings_diagnostics(&[("contract name", &name)])); + + let mut has_event = false; + let mut has_storage = false; + let mut has_init = false; + let mut has_constructor = false; + + if let MaybeModuleBody::Some(body) = module_ast.body(db) { + // TODO: Use `.iter_items_in_cfg(db, metadata.cfg_set)` when possible + // to ensure we don't loop on items that are not in the current cfg set. + let mut body_nodes: Vec<_> = body + .items_vec(db) + .iter() + .flat_map(|el| { + if let ast::ModuleItem::Enum(ref enum_ast) = el { + if enum_ast.name(db).text(db).to_string() == "Event" { + has_event = true; + + return merge_event(db, enum_ast.clone()); + } + } else if let ast::ModuleItem::Struct(ref struct_ast) = el { + if struct_ast.name(db).text(db).to_string() == "Storage" { + has_storage = true; + return merge_storage(db, struct_ast.clone()); + } + } else if let ast::ModuleItem::FreeFunction(ref fn_ast) = el { + let fn_decl = fn_ast.declaration(db); + let fn_name = fn_decl.name(db).text(db); + + if fn_name == CONSTRUCTOR_FN { + has_constructor = true; + return handle_constructor_fn(db, fn_ast); + } + + if fn_name == DOJO_INIT_FN { + has_init = true; + return handle_init_fn(db, fn_ast, &mut diagnostics); + } + } + + vec![RewriteNode::Copied(el.as_syntax_node())] + }) + .collect(); + + if !has_constructor { + let node = RewriteNode::Text( + " + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + } + " + .to_string(), + ); + + body_nodes.append(&mut vec![node]); + } + + if !has_init { + let node = RewriteNode::interpolate_patched( + DEFAULT_INIT_PATCH, + &UnorderedHashMap::from([( + "init_name".to_string(), + RewriteNode::Text(DOJO_INIT_FN.to_string()), + )]), + ); + body_nodes.append(&mut vec![node]); + } + + if !has_event { + body_nodes.append(&mut create_event()) + } + + if !has_storage { + body_nodes.append(&mut create_storage()) + } + + let mut builder = PatchBuilder::new(db, module_ast); + builder.add_modified(RewriteNode::Mapped { + node: Box::new(RewriteNode::interpolate_patched( + CONTRACT_PATCH, + &UnorderedHashMap::from([ + ("name".to_string(), RewriteNode::Text(name.to_string())), + ("body".to_string(), RewriteNode::new_modified(body_nodes)), + ]), + )), + origin: module_ast.as_syntax_node().span_without_trivia(db), + }); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("CONTRACT PATCH: {name}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); + } + + ProcMacroResult::new(TokenStream::empty()) +} +/// If a constructor is provided, we should keep the user statements. +/// We only inject the world provider initializer. +fn handle_constructor_fn(db: &dyn SyntaxGroup, fn_ast: &ast::FunctionWithBody) -> Vec { + let fn_decl = fn_ast.declaration(db); + + let params_str = params_to_str(db, fn_decl.signature(db).parameters(db)); + + let declaration_node = RewriteNode::Mapped { + node: Box::new(RewriteNode::Text(format!( + " + #[constructor] + fn constructor({}) {{ + self.world_provider.initializer(); + ", + params_str + ))), + origin: fn_ast.declaration(db).as_syntax_node().span_without_trivia(db), + }; + + let func_nodes = fn_ast + .body(db) + .statements(db) + .elements(db) + .iter() + .map(|e| RewriteNode::Mapped { + node: Box::new(RewriteNode::from(e.as_syntax_node())), + origin: e.as_syntax_node().span_without_trivia(db), + }) + .collect::>(); + + let mut nodes = vec![declaration_node]; + + nodes.extend(func_nodes); + + // Close the constructor with users statements included. + nodes.push(RewriteNode::Text("}\n".to_string())); + + nodes +} + +fn handle_init_fn( + db: &dyn SyntaxGroup, + fn_ast: &ast::FunctionWithBody, + diagnostics: &mut Vec, +) -> Vec { + let fn_decl = fn_ast.declaration(db); + + if let OptionReturnTypeClause::ReturnTypeClause(_) = fn_decl.signature(db).ret_ty(db) { + diagnostics.push_error(format!("The {} function cannot have a return type.", DOJO_INIT_FN)); + } + + let params: Vec = fn_decl + .signature(db) + .parameters(db) + .elements(db) + .iter() + .map(|p| p.as_syntax_node().get_text(db)) + .collect::>(); + + let params_str = params.join(", "); + + // Since the dojo init is meant to be called by the world, we don't need an + // interface to be generated (which adds a considerable amount of code). + let impl_node = RewriteNode::Text( + " + #[abi(per_item)] + #[generate_trait] + pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + " + .to_string(), + ); + + let declaration_node = RewriteNode::Mapped { + node: Box::new(RewriteNode::Text(format!("fn {}({}) {{", DOJO_INIT_FN, params_str))), + origin: fn_ast.declaration(db).as_syntax_node().span_without_trivia(db), + }; + + // Asserts the caller is the world, and close the init function. + let assert_world_caller_node = RewriteNode::Text( + "if starknet::get_caller_address() != \ + self.world_provider.world_dispatcher().contract_address { \ + core::panics::panic_with_byte_array(@format!(\"Only the world can init contract `{}`, \ + but caller is `{:?}`\", self.dojo_name(), starknet::get_caller_address())); }" + .to_string(), + ); + + let func_nodes = fn_ast + .body(db) + .statements(db) + .elements(db) + .iter() + .map(|e| RewriteNode::Mapped { + node: Box::new(RewriteNode::from(e.as_syntax_node())), + origin: e.as_syntax_node().span_without_trivia(db), + }) + .collect::>(); + + let mut nodes = vec![impl_node, declaration_node, assert_world_caller_node]; + nodes.extend(func_nodes); + // Close the init function + close the impl block. + nodes.push(RewriteNode::Text("}\n}".to_string())); + + nodes +} + +pub fn merge_event(db: &dyn SyntaxGroup, enum_ast: ast::ItemEnum) -> Vec { + let mut rewrite_nodes = vec![]; + + let elements = enum_ast.variants(db).elements(db); + + let variants = elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); + let variants = variants.join(",\n"); + + rewrite_nodes.push(RewriteNode::interpolate_patched( + " + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + $variants$ + } + ", + &UnorderedHashMap::from([("variants".to_string(), RewriteNode::Text(variants))]), + )); + rewrite_nodes +} + +pub fn create_event() -> Vec { + vec![RewriteNode::Text( + " + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + } + " + .to_string(), + )] +} + +pub fn merge_storage(db: &dyn SyntaxGroup, struct_ast: ast::ItemStruct) -> Vec { + let mut rewrite_nodes = vec![]; + + let elements = struct_ast.members(db).elements(db); + + let members = elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); + let members = members.join(",\n"); + + rewrite_nodes.push(RewriteNode::interpolate_patched( + " + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + $members$ + } + ", + &UnorderedHashMap::from([("members".to_string(), RewriteNode::Text(members))]), + )); + rewrite_nodes +} + +pub fn create_storage() -> Vec { + vec![RewriteNode::Text( + " + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + } + " + .to_string(), + )] +} + +/// Converts parameter list to it's string representation. +pub fn params_to_str(db: &dyn SyntaxGroup, param_list: ast::ParamList) -> String { + let params = param_list + .elements(db) + .iter() + .map(|param| param.as_syntax_node().get_text(db)) + .collect::>(); + + params.join(", ") +} diff --git a/crates/dojo/macros/src/attributes/dojo_event.rs b/crates/dojo/macros/src/attributes/dojo_event.rs new file mode 100644 index 0000000000..645b2376cf --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_event.rs @@ -0,0 +1,131 @@ +//! `dojo_event` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use super::constants::DOJO_EVENT_ATTR; +use super::struct_parser::{ + compute_unique_hash, handle_struct_attribute_macro, parse_members, serialize_keys_and_values, + validate_attributes, validate_namings_diagnostics, +}; +use crate::attributes::struct_parser::remove_derives; +use crate::derives::{extract_derive_attr_names, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE}; +use crate::diagnostic_ext::DiagnosticsExt; + +const EVENT_PATCH: &str = include_str!("./patches/event.patch.cairo"); + +#[attribute_macro] +pub fn dojo_event(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_event_attribute_macro(token_stream) +} + +pub fn handle_event_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + handle_struct_attribute_macro(token_stream, from_struct) +} + +pub fn from_struct(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> ProcMacroResult { + let mut diagnostics = vec![]; + + let event_name = struct_ast.name(db).as_syntax_node().get_text(db).trim().to_string(); + + diagnostics.extend(validate_attributes(db, &struct_ast.attributes(db), DOJO_EVENT_ATTR)); + + diagnostics.extend(validate_namings_diagnostics(&[("event name", &event_name)])); + + let members = parse_members(db, &struct_ast.members(db).elements(db), &mut diagnostics); + + let mut serialized_keys: Vec = vec![]; + let mut serialized_values: Vec = vec![]; + + serialize_keys_and_values(&members, &mut serialized_keys, &mut serialized_values); + + if serialized_keys.is_empty() { + diagnostics.push_error("Event must define at least one #[key] attribute".to_string()); + } + + if serialized_values.is_empty() { + diagnostics + .push_error("Event must define at least one member that is not a key".to_string()); + } + + let members_values = members + .iter() + .filter_map(|m| { + if m.key { + None + } else { + Some(RewriteNode::Text(format!("pub {}: {},\n", m.name, m.ty))) + } + }) + .collect::>(); + + let member_names = members + .iter() + .map(|member| RewriteNode::Text(format!("{},\n", member.name.clone()))) + .collect::>(); + + let derive_attr_names = extract_derive_attr_names( + db, + &mut diagnostics, + struct_ast.attributes(db).query_attr(db, "derive"), + ); + + let has_introspect = derive_attr_names.contains(&DOJO_INTROSPECT_DERIVE.to_string()); + let has_introspect_packed = derive_attr_names.contains(&DOJO_PACKED_DERIVE.to_string()); + let has_drop = derive_attr_names.contains(&"Drop".to_string()); + let has_serde = derive_attr_names.contains(&"Serde".to_string()); + + if has_introspect && has_introspect_packed { + diagnostics.push_error( + "Event cannot derive from both Introspect and IntrospectPacked. Only Introspect is \ + allowed." + .to_string(), + ); + } + + if !has_introspect && !has_drop && !has_serde { + diagnostics.push_error("Event must derive from Introspect, Drop and Serde.".to_string()); + } + + let derive_node = if has_introspect { + RewriteNode::Text(format!("#[derive({})]", DOJO_INTROSPECT_DERIVE)) + } else { + RewriteNode::empty() + }; + + // Must remove the derives from the original struct since they would create duplicates + // with the derives of other plugins. + let original_struct = remove_derives(db, struct_ast); + + let unique_hash = + compute_unique_hash(db, &event_name, false, &struct_ast.members(db).elements(db)) + .to_string(); + + let dojo_node = RewriteNode::interpolate_patched( + EVENT_PATCH, + &UnorderedHashMap::from([ + ("derive_node".to_string(), derive_node), + ("original_struct".to_string(), original_struct), + ("type_name".to_string(), RewriteNode::Text(event_name.clone())), + ("member_names".to_string(), RewriteNode::new_modified(member_names)), + ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), + ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), + ("unique_hash".to_string(), RewriteNode::Text(unique_hash)), + ("members_values".to_string(), RewriteNode::new_modified(members_values)), + ]), + ); + + let mut builder = PatchBuilder::new(db, struct_ast); + builder.add_modified(dojo_node); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("EVENT PATCH: {event_name}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} diff --git a/crates/dojo/macros/src/attributes/dojo_model.rs b/crates/dojo/macros/src/attributes/dojo_model.rs new file mode 100644 index 0000000000..2654707298 --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_model.rs @@ -0,0 +1,197 @@ +//! `dojo_model` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; +use starknet::core::utils::get_selector_from_name; + +use super::constants::DOJO_MODEL_ATTR; +use super::struct_parser::{ + compute_unique_hash, handle_struct_attribute_macro, parse_members, serialize_member_ty, + validate_attributes, validate_namings_diagnostics, Member, +}; +use crate::attributes::struct_parser::remove_derives; +use crate::derives::{extract_derive_attr_names, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE}; +use crate::diagnostic_ext::DiagnosticsExt; + +const MODEL_CODE_PATCH: &str = include_str!("./patches/model.patch.cairo"); +const MODEL_FIELD_CODE_PATCH: &str = include_str!("./patches/model_field_store.patch.cairo"); + +#[attribute_macro] +pub fn dojo_model(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_model_attribute_macro(token_stream) +} + +pub fn handle_model_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + handle_struct_attribute_macro(token_stream, from_struct) +} + +pub fn from_struct(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> ProcMacroResult { + let mut diagnostics = vec![]; + + let model_type = struct_ast.name(db).as_syntax_node().get_text(db).trim().to_string(); + + diagnostics.extend(validate_attributes(db, &struct_ast.attributes(db), DOJO_MODEL_ATTR)); + diagnostics.extend(validate_namings_diagnostics(&[("model name", &model_type)])); + + let mut values: Vec = vec![]; + let mut keys: Vec = vec![]; + let mut members_values: Vec = vec![]; + let mut key_types: Vec = vec![]; + let mut key_attrs: Vec = vec![]; + + let mut serialized_keys: Vec = vec![]; + let mut serialized_values: Vec = vec![]; + let mut field_accessors: Vec = vec![]; + + let members = parse_members(db, &struct_ast.members(db).elements(db), &mut diagnostics); + + members.iter().for_each(|member| { + if member.key { + keys.push(member.clone()); + key_types.push(member.ty.clone()); + key_attrs.push(format!("*self.{}", member.name.clone())); + serialized_keys.push(serialize_member_ty(member, true)); + } else { + values.push(member.clone()); + serialized_values.push(serialize_member_ty(member, true)); + members_values + .push(RewriteNode::Text(format!("pub {}: {},\n", member.name, member.ty))); + field_accessors.push(generate_field_accessors(model_type.clone(), member)); + } + }); + + if keys.is_empty() { + diagnostics.push_error("Model must define at least one #[key] attribute".to_string()); + } + + if values.is_empty() { + diagnostics + .push_error("Model must define at least one member that is not a key".to_string()); + } + + if !diagnostics.is_empty() { + return ProcMacroResult::new(TokenStream::empty()) + .with_diagnostics(Diagnostics::new(diagnostics)); + } + + let (keys_to_tuple, key_type) = if keys.len() > 1 { + (format!("({})", key_attrs.join(", ")), format!("({})", key_types.join(", "))) + } else { + (key_attrs.first().unwrap().to_string(), key_types.first().unwrap().to_string()) + }; + + let derive_attr_names = extract_derive_attr_names( + db, + &mut diagnostics, + struct_ast.attributes(db).query_attr(db, "derive"), + ); + + let has_introspect = derive_attr_names.contains(&DOJO_INTROSPECT_DERIVE.to_string()); + let has_introspect_packed = derive_attr_names.contains(&DOJO_PACKED_DERIVE.to_string()); + let has_drop = derive_attr_names.contains(&"Drop".to_string()); + let has_serde = derive_attr_names.contains(&"Serde".to_string()); + + if has_introspect && has_introspect_packed { + diagnostics.push_error( + "Model cannot derive from both Introspect and IntrospectPacked.".to_string(), + ); + } + + #[allow(clippy::nonminimal_bool)] + if !(has_introspect || has_introspect_packed) && !has_drop && !has_serde { + diagnostics.push_error( + "Model must derive from Introspect or IntrospectPacked, Drop and Serde.".to_string(), + ); + } + + let derive_node = if has_introspect { + RewriteNode::Text(format!("#[derive({})]", DOJO_INTROSPECT_DERIVE)) + } else if has_introspect_packed { + RewriteNode::Text(format!("#[derive({})]", DOJO_PACKED_DERIVE)) + } else { + RewriteNode::empty() + }; + + // Must remove the derives from the original struct since they would create duplicates + // with the derives of other plugins. + let original_struct = remove_derives(db, struct_ast); + + // Reuse the same derive attributes for ModelValue (except Introspect/IntrospectPacked). + let model_value_derive_attr_names = derive_attr_names + .iter() + .map(|d| d.as_str()) + .filter(|&d| d != DOJO_INTROSPECT_DERIVE && d != DOJO_PACKED_DERIVE) + .collect::>() + .join(", "); + + let unique_hash = compute_unique_hash( + db, + &model_type, + has_introspect_packed, + &struct_ast.members(db).elements(db), + ) + .to_string(); + + let dojo_node = RewriteNode::interpolate_patched( + MODEL_CODE_PATCH, + &UnorderedHashMap::from([ + ("derive_node".to_string(), derive_node), + ("original_struct".to_string(), original_struct), + ("model_type".to_string(), RewriteNode::Text(model_type.clone())), + ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), + ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), + ("keys_to_tuple".to_string(), RewriteNode::Text(keys_to_tuple)), + ("key_type".to_string(), RewriteNode::Text(key_type)), + ("members_values".to_string(), RewriteNode::new_modified(members_values)), + ("field_accessors".to_string(), RewriteNode::new_modified(field_accessors)), + ( + "model_value_derive_attr_names".to_string(), + RewriteNode::Text(model_value_derive_attr_names), + ), + ("unique_hash".to_string(), RewriteNode::Text(unique_hash)), + ]), + ); + + let mut builder = PatchBuilder::new(db, struct_ast); + builder.add_modified(dojo_node); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("MODEL PATCH: {model_type}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} + +/// Generates field accessors (`get_[field_name]` and `set_[field_name]`) for every +/// fields of a model. +/// +/// # Arguments +/// +/// * `model_name` - the model name. +/// * `param_keys` - coma separated model keys with the format `KEY_NAME: KEY_TYPE`. +/// * `serialized_param_keys` - code to serialize model keys in a `serialized` felt252 array. +/// * `member` - information about the field for which to generate accessors. +/// +/// # Returns +/// A [`RewriteNode`] containing accessors code. +fn generate_field_accessors(model_type: String, member: &Member) -> RewriteNode { + RewriteNode::interpolate_patched( + MODEL_FIELD_CODE_PATCH, + &UnorderedHashMap::from([ + ("model_type".to_string(), RewriteNode::Text(model_type)), + ( + "field_selector".to_string(), + RewriteNode::Text( + get_selector_from_name(&member.name).expect("invalid member name").to_string(), + ), + ), + ("field_name".to_string(), RewriteNode::Text(member.name.clone())), + ("field_type".to_string(), RewriteNode::Text(member.ty.clone())), + ]), + ) +} diff --git a/crates/dojo/macros/src/attributes/mod.rs b/crates/dojo/macros/src/attributes/mod.rs new file mode 100644 index 0000000000..a243e83ac9 --- /dev/null +++ b/crates/dojo/macros/src/attributes/mod.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod dojo_contract; +pub mod dojo_event; +pub mod dojo_model; +pub mod struct_parser; diff --git a/crates/dojo/macros/src/attributes/patches/contract.patch.cairo b/crates/dojo/macros/src/attributes/patches/contract.patch.cairo new file mode 100644 index 0000000000..46ee7353b1 --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/contract.patch.cairo @@ -0,0 +1,35 @@ +#[starknet::contract] +pub mod $name$ { + use dojo::contract::components::world_provider::{world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, IWorldProvider}; + use dojo::contract::components::upgradeable::upgradeable_cpt; + use dojo::contract::IContract; + use dojo::meta::IDeployedResource; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[abi(embed_v0)] + pub impl $name$__ContractImpl of IContract {} + + #[abi(embed_v0)] + pub impl $name$__DeployedContractImpl of IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "$name$" + } + } + + #[generate_trait] + impl $name$InternalImpl of $name$InternalTrait { + fn world(self: @ContractState, namespace: @ByteArray) -> dojo::world::storage::WorldStorage { + dojo::world::WorldStorageTrait::new(self.world_provider.world_dispatcher(), namespace) + } + } + + $body$ +} diff --git a/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo b/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo new file mode 100644 index 0000000000..435bad567e --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo @@ -0,0 +1,14 @@ +#[abi(per_item)] +#[generate_trait] +pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + fn $init_name$(self: @ContractState) { + if starknet::get_caller_address() != self.world_provider.world_dispatcher().contract_address { + core::panics::panic_with_byte_array( + @format!("Only the world can init contract `{}`, but caller is `{:?}`", + self.dojo_name(), + starknet::get_caller_address(), + )); + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/event.patch.cairo b/crates/dojo/macros/src/attributes/patches/event.patch.cairo new file mode 100644 index 0000000000..a6275ab39c --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/event.patch.cairo @@ -0,0 +1,76 @@ +$derive_node$ +$original_struct$ + +// EventValue on it's own does nothing since events are always emitted and +// never read from the storage. However, it's required by the ABI to +// ensure that the event definition contains both keys and values easily distinguishable. +// Only derives strictly required traits. +#[derive(Drop, Serde)] +pub struct $type_name$Value { + $members_values$ +} + +pub impl $type_name$Definition of dojo::event::EventDefinition<$type_name$>{ + #[inline(always)] + fn name() -> ByteArray { + "$type_name$" + } +} + +pub impl $type_name$ModelParser of dojo::model::model::ModelParser<$type_name$>{ + fn serialize_keys(self: @$type_name$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_keys$ + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @$type_name$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $type_name$EventImpl = dojo::event::event::EventImpl<$type_name$>; + +#[starknet::contract] +pub mod e_$type_name$ { + use super::$type_name$; + use super::$type_name$Value; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl $type_name$__DeployedEventImpl = dojo::event::component::IDeployedEventImpl; + + #[abi(embed_v0)] + impl $type_name$__StoredEventImpl = dojo::event::component::IStoredEventImpl; + + #[abi(embed_v0)] + impl $type_name$__EventImpl = dojo::event::component::IEventImpl; + + #[abi(per_item)] + #[generate_trait] + impl $type_name$Impl of I$type_name${ + // Ensures the ABI contains the Event struct, since it's never used + // by systems directly. + #[external(v0)] + fn ensure_abi(self: @ContractState, event: $type_name$) { + let _event = event; + } + + // Outputs EventValue to allow a simple diff from the ABI compared to the + // event to retrieved the keys of an event. + #[external(v0)] + fn ensure_values(self: @ContractState, value: $type_name$Value) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on event and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) { + let _hash = $unique_hash$; + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/model.patch.cairo b/crates/dojo/macros/src/attributes/patches/model.patch.cairo new file mode 100644 index 0000000000..d3662f14a9 --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/model.patch.cairo @@ -0,0 +1,120 @@ +$derive_node$ +$original_struct$ + +#[derive($model_value_derive_attr_names$)] +pub struct $model_type$Value { + $members_values$ +} + +type $model_type$KeyType = $key_type$; + +pub impl $model_type$KeyParser of dojo::model::model::KeyParser<$model_type$, $model_type$KeyType>{ + #[inline(always)] + fn parse_key(self: @$model_type$) -> $model_type$KeyType { + $keys_to_tuple$ + } +} + +impl $model_type$ModelValueKey of dojo::model::model_value::ModelValueKey<$model_type$Value, $model_type$KeyType> { +} + +// Impl to get the static definition of a model +pub mod m_$model_type$_definition { + use super::$model_type$; + pub impl $model_type$DefinitionImpl of dojo::model::ModelDefinition{ + #[inline(always)] + fn name() -> ByteArray { + "$model_type$" + } + + #[inline(always)] + fn layout() -> dojo::meta::Layout { + dojo::meta::Introspect::<$model_type$>::layout() + } + + #[inline(always)] + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = dojo::meta::Introspect::<$model_type$>::ty() { + s + } + else { + panic!("Model `$model_type$`: invalid schema.") + } + } + + #[inline(always)] + fn size() -> Option { + dojo::meta::Introspect::<$model_type$>::size() + } + } +} + +pub impl $model_type$Definition = m_$model_type$_definition::$model_type$DefinitionImpl<$model_type$>; +pub impl $model_type$ModelValueDefinition = m_$model_type$_definition::$model_type$DefinitionImpl<$model_type$Value>; + +pub impl $model_type$ModelParser of dojo::model::model::ModelParser<$model_type$>{ + fn serialize_keys(self: @$model_type$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_keys$ + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @$model_type$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $model_type$ModelValueParser of dojo::model::model_value::ModelValueParser<$model_type$Value>{ + fn serialize_values(self: @$model_type$Value) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $model_type$ModelImpl = dojo::model::model::ModelImpl<$model_type$>; +pub impl $model_type$ModelValueImpl = dojo::model::model_value::ModelValueImpl<$model_type$Value>; + +#[starknet::contract] +pub mod m_$model_type$ { + use super::$model_type$; + use super::$model_type$Value; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl $model_type$__DojoDeployedModelImpl = dojo::model::component::IDeployedModelImpl; + + #[abi(embed_v0)] + impl $model_type$__DojoStoredModelImpl = dojo::model::component::IStoredModelImpl; + + #[abi(embed_v0)] + impl $model_type$__DojoModelImpl = dojo::model::component::IModelImpl; + + #[abi(per_item)] + #[generate_trait] + impl $model_type$Impl of I$model_type${ + // Ensures the ABI contains the Model struct, even if never used + // into as a system input. + #[external(v0)] + fn ensure_abi(self: @ContractState, model: $model_type$) { + let _model = model; + } + + // Outputs ModelValue to allow a simple diff from the ABI compared to the + // model to retrieved the keys of a model. + #[external(v0)] + fn ensure_values(self: @ContractState, value: $model_type$Value) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on model and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) { + let _hash = $unique_hash$; + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo b/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo new file mode 100644 index 0000000000..8fa466617f --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo @@ -0,0 +1,15 @@ + fn get_$field_name$(self: @S, key: $model_type$KeyType) -> $field_type$ { + $model_type$Store::get_member(self, key, $field_selector$) + } + + fn get_$field_name$_from_id(self: @S, entity_id: felt252) -> $field_type$ { + $model_type$ModelValueStore::get_member_from_id(self, entity_id, $field_selector$) + } + + fn update_$field_name$(ref self: S, key: $model_type$KeyType, value: $field_type$) { + $model_type$Store::update_member(ref self, key, $field_selector$, value); + } + + fn update_$field_name$_from_id(ref self: S, entity_id: felt252, value: $field_type$) { + $model_type$ModelValueStore::update_member_from_id(ref self, entity_id, $field_selector$, value); + } diff --git a/crates/dojo/macros/src/attributes/struct_parser.rs b/crates/dojo/macros/src/attributes/struct_parser.rs new file mode 100644 index 0000000000..dcb25abe47 --- /dev/null +++ b/crates/dojo/macros/src/attributes/struct_parser.rs @@ -0,0 +1,214 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_macro::{Diagnostic, ProcMacroResult, Severity, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::node::ast::{self, AttributeList, Member as MemberAst}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::kind::SyntaxKind::ItemStruct; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use dojo_types::naming; +use dojo_types::naming::compute_bytearray_hash; +use serde::{Deserialize, Serialize}; +use starknet_crypto::{poseidon_hash_many, Felt}; + +use super::constants::DOJO_ATTR_NAMES; +use crate::diagnostic_ext::DiagnosticsExt; + +/// Represents a member of a struct. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Member { + // Name of the member. + pub name: String, + // Type of the member. + pub ty: String, + // Whether the member is a key. + pub key: bool, +} + +pub fn parse_members( + db: &dyn SyntaxGroup, + members: &[MemberAst], + diagnostics: &mut Vec, +) -> Vec { + members + .iter() + .filter_map(|member_ast| { + let member = Member { + name: member_ast.name(db).text(db).to_string(), + ty: member_ast + .type_clause(db) + .ty(db) + .as_syntax_node() + .get_text(db) + .trim() + .to_string(), + key: member_ast.has_attr(db, "key"), + }; + + // validate key member + if member.key && member.ty == "u256" { + diagnostics.push(Diagnostic { + message: "Key is only supported for core types that are 1 felt long once \ + serialized. `u256` is a struct of 2 u128, hence not supported." + .into(), + severity: Severity::Error, + }); + None + } else { + Some(member) + } + }) + .collect::>() +} + +pub fn serialize_keys_and_values( + members: &[Member], + serialized_keys: &mut Vec, + serialized_values: &mut Vec, +) { + members.iter().for_each(|member| { + if member.key { + serialized_keys.push(serialize_member_ty(member, true)); + } else { + serialized_values.push(serialize_member_ty(member, true)); + } + }); +} + +/// Creates a [`RewriteNode`] for the member type serialization. +/// +/// # Arguments +/// +/// * member: The member to serialize. +pub fn serialize_member_ty(member: &Member, with_self: bool) -> RewriteNode { + RewriteNode::Text(format!( + "core::serde::Serde::serialize({}{}, ref serialized);\n", + if with_self { "self." } else { "@" }, + member.name + )) +} + +pub fn deserialize_member_ty(member: &Member, input_name: &str) -> RewriteNode { + RewriteNode::Text(format!( + "let {} = core::serde::Serde::<{}>::deserialize(ref {input_name})?;\n", + member.name, member.ty + )) +} + +/// Validates the namings of the attributes. +/// +/// # Arguments +/// +/// * namings: A list of tuples containing the id and value of the attribute. +/// +/// # Returns +/// +/// A vector of diagnostics. +pub fn validate_namings_diagnostics(namings: &[(&str, &str)]) -> Vec { + let mut diagnostics = vec![]; + + for (id, value) in namings { + if !naming::is_name_valid(value) { + diagnostics.push_error(format!( + "The {id} '{value}' can only contain characters (a-z/A-Z), digits (0-9) and \ + underscore (_)." + )); + } + } + + diagnostics +} + +/// Removes the derives from the original struct. +pub fn remove_derives(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> RewriteNode { + let mut out_lines = vec![]; + + let struct_str = struct_ast.as_syntax_node().get_text_without_trivia(db).to_string(); + + for l in struct_str.lines() { + if !l.starts_with("#[derive") { + out_lines.push(l); + } + } + + RewriteNode::Text(out_lines.join("\n")) +} + +/// Validates the attributes of a Dojo attribute. +/// +/// Parameters: +/// * db: The semantic database. +/// * module_ast: The AST of the contract module. +/// +/// Returns: +/// * A vector of diagnostics. +pub fn validate_attributes( + db: &dyn SyntaxGroup, + attribute_list: &AttributeList, + ref_attribute: &str, +) -> Vec { + let mut diagnostics = vec![]; + + for attribute in DOJO_ATTR_NAMES { + if attribute == ref_attribute { + if attribute_list.query_attr(db, attribute).first().is_some() { + diagnostics.push_error(format!( + "Only one {} attribute is allowed per module.", + ref_attribute + )); + } + } else { + if attribute_list.query_attr(db, attribute).first().is_some() { + diagnostics.push_error(format!( + "A {} can't be used together with a {}.", + ref_attribute, attribute + )); + } + } + } + + diagnostics +} + +/// Compute a unique hash based on the element name and types and names of members. +/// This hash is used in element contracts to ensure uniqueness. +pub fn compute_unique_hash( + db: &dyn SyntaxGroup, + element_name: &str, + is_packed: bool, + members: &[MemberAst], +) -> Felt { + let mut hashes = + vec![if is_packed { Felt::ONE } else { Felt::ZERO }, compute_bytearray_hash(element_name)]; + hashes.extend( + members + .iter() + .map(|m| { + poseidon_hash_many(&[ + compute_bytearray_hash(&m.name(db).text(db).to_string()), + compute_bytearray_hash( + m.type_clause(db).ty(db).as_syntax_node().get_text(db).trim(), + ), + ]) + }) + .collect::>(), + ); + poseidon_hash_many(&hashes) +} + +pub fn handle_struct_attribute_macro( + token_stream: TokenStream, + from_struct: fn(&dyn SyntaxGroup, &ast::ItemStruct) -> ProcMacroResult, +) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (root_node, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in root_node.descendants(&db) { + if n.kind(&db) == ItemStruct { + let struct_ast = ast::ItemStruct::from_syntax_node(&db, n); + return from_struct(&db, &struct_ast); + } + } + + ProcMacroResult::new(TokenStream::empty()) +} diff --git a/crates/dojo/macros/src/derives/introspect/layout.rs b/crates/dojo/macros/src/derives/introspect/layout.rs new file mode 100644 index 0000000000..c38b5d8042 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/layout.rs @@ -0,0 +1,338 @@ +use cairo_lang_macro::Diagnostic; +use cairo_lang_syntax::node::ast::{Expr, ItemEnum, ItemStruct, OptionTypeClause, TypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use starknet::core::utils::get_selector_from_name; + +use super::utils::{ + get_array_item_type, get_tuple_item_types, is_array, is_byte_array, is_tuple, + is_unsupported_option_type, primitive_type_introspection, +}; +use crate::diagnostic_ext::DiagnosticsExt; + +/// build the full layout for every field in the Struct. +pub fn build_field_layouts( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: &ItemStruct, +) -> String { + struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + let field_name = m.name(db).text(db); + let field_selector = get_selector_from_name(&field_name.to_string()).unwrap(); + let field_layout = get_layout_from_type_clause(db, diagnostics, &m.type_clause(db)); + Some(format!( + "dojo::meta::FieldLayout {{ + selector: {field_selector}, + layout: {field_layout} + }}" + )) + }) + .collect::>() + .join(",\n") +} + +/// build the full layout for every variant in the Enum. +/// Note that every variant may have a different associated data type. +pub fn build_variant_layouts( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: &ItemEnum, +) -> String { + enum_ast + .variants(db) + .elements(db) + .iter() + .enumerate() + .map(|(i, v)| { + let selector = format!("{i}"); + + let variant_layout = match v.type_clause(db) { + OptionTypeClause::Empty(_) => { + "dojo::meta::Layout::Fixed(array![].span())".to_string() + } + OptionTypeClause::TypeClause(type_clause) => { + get_layout_from_type_clause(db, diagnostics, &type_clause) + } + }; + + format!( + "dojo::meta::FieldLayout {{ + selector: {selector}, + layout: {variant_layout} + }}" + ) + }) + .collect::>() + .join(",\n") +} + +/// Build a field layout describing the provided type clause. +pub fn get_layout_from_type_clause( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + type_clause: &TypeClause, +) -> String { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db); + build_item_layout_from_type(diagnostics, &path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db); + build_tuple_layout_from_type(diagnostics, &tuple_type) + } + _ => { + diagnostics.push_error("Unexpected expression for variant data type.".to_string()); + "ERROR".to_string() + } + } +} + +/// Build the array layout describing the provided array type. +/// item_type could be something like `Array` for example. +pub fn build_array_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + let array_item_type = get_array_item_type(item_type); + + if is_tuple(&array_item_type) { + format!( + "dojo::meta::Layout::Array( + array![ + {} + ].span() + )", + build_item_layout_from_type(diagnostics, &array_item_type) + ) + } else if is_array(&array_item_type) { + format!( + "dojo::meta::Layout::Array( + array![ + {} + ].span() + )", + build_array_layout_from_type(diagnostics, &array_item_type) + ) + } else { + format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type) + } +} + +/// Build the tuple layout describing the provided tuple type. +/// item_type could be something like (u8, u32, u128) for example. +pub fn build_tuple_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + let tuple_items = get_tuple_item_types(item_type) + .iter() + .map(|x| build_item_layout_from_type(diagnostics, x)) + .collect::>() + .join(",\n"); + format!( + "dojo::meta::Layout::Tuple( + array![ + {} + ].span() + )", + tuple_items + ) +} + +/// Build the layout describing the provided type. +/// item_type could be any type (array, tuple, struct, ...) +pub fn build_item_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + if is_array(item_type) { + build_array_layout_from_type(diagnostics, item_type) + } else if is_tuple(item_type) { + build_tuple_layout_from_type(diagnostics, item_type) + } else { + // For Option, T cannot be a tuple + if is_unsupported_option_type(item_type) { + diagnostics.push_error( + "Option cannot be used with tuples. Prefer using a struct.".to_string(), + ); + } + + format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type) + } +} + +pub fn is_custom_layout(layout: &str) -> bool { + layout.starts_with("dojo::meta::introspect::Introspect::") +} + +pub fn build_packed_struct_layout( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: &ItemStruct, +) -> String { + let layouts = struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + Some(get_packed_field_layout_from_type_clause(db, diagnostics, &m.type_clause(db))) + }) + .flatten() + .collect::>(); + + if layouts.iter().any(|v| is_custom_layout(v.as_str())) { + generate_cairo_code_for_fixed_layout_with_custom_types(&layouts) + } else { + format!( + "dojo::meta::Layout::Fixed( + array![ + {} + ].span() + )", + layouts.join(",") + ) + } +} + +pub fn generate_cairo_code_for_fixed_layout_with_custom_types(layouts: &[String]) -> String { + let layouts_repr = layouts + .iter() + .map(|l| { + if is_custom_layout(l) { + l.to_string() + } else { + format!("dojo::meta::Layout::Fixed(array![{l}].span())") + } + }) + .collect::>() + .join(",\n"); + + format!( + "let mut layouts = array![ + {layouts_repr} + ]; + let mut merged_layout = ArrayTrait::::new(); + + loop {{ + match ArrayTrait::pop_front(ref layouts) {{ + Option::Some(mut layout) => {{ + match layout {{ + dojo::meta::Layout::Fixed(mut l) => {{ + loop {{ + match SpanTrait::pop_front(ref l) {{ + Option::Some(x) => merged_layout.append(*x), + Option::None(_) => {{ break; }} + }}; + }}; + }}, + _ => panic!(\"A packed model layout must contain Fixed layouts only.\"), + }}; + }}, + Option::None(_) => {{ break; }} + }}; + }}; + + dojo::meta::Layout::Fixed(merged_layout.span()) + ", + ) +} + +// +pub fn build_packed_enum_layout( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: &ItemEnum, +) -> String { + // to be packable, all variants data must have the same size. + // as this point has already been checked before calling `build_packed_enum_layout`, + // just use the first variant to generate the fixed layout. + let elements = enum_ast.variants(db).elements(db); + let mut variant_layout = if elements.is_empty() { + vec![] + } else { + match elements.first().unwrap().type_clause(db) { + OptionTypeClause::Empty(_) => vec![], + OptionTypeClause::TypeClause(type_clause) => { + get_packed_field_layout_from_type_clause(db, diagnostics, &type_clause) + } + } + }; + + // don't forget the store the variant value + variant_layout.insert(0, "8".to_string()); + + if variant_layout.iter().any(|v| is_custom_layout(v.as_str())) { + generate_cairo_code_for_fixed_layout_with_custom_types(&variant_layout) + } else { + format!( + "dojo::meta::Layout::Fixed( + array![ + {} + ].span() + )", + variant_layout.join(",") + ) + } +} + +// +pub fn get_packed_field_layout_from_type_clause( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + type_clause: &TypeClause, +) -> Vec { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db); + get_packed_item_layout_from_type(diagnostics, path_type.trim()) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db); + get_packed_tuple_layout_from_type(diagnostics, &tuple_type) + } + _ => { + diagnostics.push_error("Unexpected expression for variant data type.".to_string()); + vec!["ERROR".to_string()] + } + } +} + +// +pub fn get_packed_item_layout_from_type( + diagnostics: &mut Vec, + item_type: &str, +) -> Vec { + if is_array(item_type) || is_byte_array(item_type) { + diagnostics.push_error("Array field cannot be packed.".to_string()); + vec!["ERROR".to_string()] + } else if is_tuple(item_type) { + get_packed_tuple_layout_from_type(diagnostics, item_type) + } else { + let primitives = primitive_type_introspection(); + + if let Some(p) = primitives.get(item_type) { + vec![p.1.iter().map(|x| x.to_string()).collect::>().join(",")] + } else { + // as we cannot verify that an enum/struct custom type is packable, + // we suppose it is and let the user verify this. + // If it's not the case, the Dojo model layout function will panic. + vec![format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type)] + } + } +} + +// +pub fn get_packed_tuple_layout_from_type( + diagnostics: &mut Vec, + item_type: &str, +) -> Vec { + get_tuple_item_types(item_type) + .iter() + .flat_map(|x| get_packed_item_layout_from_type(diagnostics, x)) + .collect::>() +} diff --git a/crates/dojo/macros/src/derives/introspect/mod.rs b/crates/dojo/macros/src/derives/introspect/mod.rs new file mode 100644 index 0000000000..e01c4e4cd2 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/mod.rs @@ -0,0 +1,154 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_macro::Diagnostic; +use cairo_lang_syntax::node::ast::{ + GenericParam, ItemEnum, ItemStruct, OptionWrappedGenericParamList, +}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::Terminal; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use crate::diagnostic_ext::DiagnosticsExt; + +mod layout; +mod size; +mod ty; +mod utils; + +/// Generate the introspect of a Struct +pub fn handle_introspect_struct( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: ItemStruct, + packed: bool, +) -> RewriteNode { + let struct_name = struct_ast.name(db).text(db).into(); + let struct_size = size::compute_struct_layout_size(db, &struct_ast, packed); + let ty = ty::build_struct_ty(db, &struct_name, &struct_ast); + + let layout = if packed { + layout::build_packed_struct_layout(db, diagnostics, &struct_ast) + } else { + format!( + "dojo::meta::Layout::Struct( + array![ + {} + ].span() + )", + layout::build_field_layouts(db, diagnostics, &struct_ast) + ) + }; + + let (gen_types, gen_impls) = build_generic_types_and_impls(db, struct_ast.generic_params(db)); + + generate_introspect(&struct_name, &struct_size, &gen_types, gen_impls, &layout, &ty) +} + +/// Generate the introspect of a Enum +pub fn handle_introspect_enum( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: ItemEnum, + packed: bool, +) -> RewriteNode { + let enum_name = enum_ast.name(db).text(db).into(); + let variant_sizes = size::compute_enum_variant_sizes(db, &enum_ast); + + let layout = if packed { + if size::is_enum_packable(&variant_sizes) { + layout::build_packed_enum_layout(db, diagnostics, &enum_ast) + } else { + diagnostics.push_error( + "To be packed, all variants must have fixed layout of same size.".to_string(), + ); + "ERROR".to_string() + } + } else { + format!( + "dojo::meta::Layout::Enum( + array![ + {} + ].span() + )", + layout::build_variant_layouts(db, diagnostics, &enum_ast) + ) + }; + + let (gen_types, gen_impls) = build_generic_types_and_impls(db, enum_ast.generic_params(db)); + let enum_size = size::compute_enum_layout_size(&variant_sizes, packed); + let ty = ty::build_enum_ty(db, &enum_name, &enum_ast); + + generate_introspect(&enum_name, &enum_size, &gen_types, gen_impls, &layout, &ty) +} + +/// Generate the introspect impl for a Struct or an Enum, +/// based on its name, size, layout and Ty. +fn generate_introspect( + name: &String, + size: &String, + generic_types: &[String], + generic_impls: String, + layout: &String, + ty: &String, +) -> RewriteNode { + RewriteNode::interpolate_patched( + " +impl $name$Introspect<$generics$> of dojo::meta::introspect::Introspect<$name$<$generics_types$>> \ + { + #[inline(always)] + fn size() -> Option { + $size$ + } + + fn layout() -> dojo::meta::Layout { + $layout$ + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + $ty$ + } +} + ", + &UnorderedHashMap::from([ + ("name".to_string(), RewriteNode::Text(name.to_string())), + ("generics".to_string(), RewriteNode::Text(generic_impls)), + ("generics_types".to_string(), RewriteNode::Text(generic_types.join(", "))), + ("size".to_string(), RewriteNode::Text(size.to_string())), + ("layout".to_string(), RewriteNode::Text(layout.to_string())), + ("ty".to_string(), RewriteNode::Text(ty.to_string())), + ]), + ) +} + +// Extract generic type information and build the +// type and impl information to add to the generated introspect +fn build_generic_types_and_impls( + db: &dyn SyntaxGroup, + generic_params: OptionWrappedGenericParamList, +) -> (Vec, String) { + let generic_types = + if let OptionWrappedGenericParamList::WrappedGenericParamList(params) = generic_params { + params + .generic_params(db) + .elements(db) + .iter() + .filter_map(|el| { + if let GenericParam::Type(typ) = el { + Some(typ.name(db).text(db).to_string()) + } else { + None + } + }) + .collect::>() + } else { + vec![] + }; + + let generic_impls = generic_types + .iter() + .map(|g| format!("{g}, impl {g}Introspect: dojo::meta::introspect::Introspect<{g}>")) + .collect::>() + .join(", "); + + (generic_types, generic_impls) +} diff --git a/crates/dojo/macros/src/derives/introspect/size.rs b/crates/dojo/macros/src/derives/introspect/size.rs new file mode 100644 index 0000000000..efb81ea25d --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/size.rs @@ -0,0 +1,200 @@ +use cairo_lang_syntax::node::ast::{Expr, ItemEnum, ItemStruct, OptionTypeClause, TypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::TypedSyntaxNode; + +use super::utils::{ + get_tuple_item_types, is_array, is_byte_array, is_tuple, primitive_type_introspection, +}; + +pub fn compute_struct_layout_size( + db: &dyn SyntaxGroup, + struct_ast: &ItemStruct, + is_packed: bool, +) -> String { + let mut cumulated_sizes = 0; + let mut is_dynamic_size = false; + + let mut sizes = struct_ast + .members(db) + .elements(db) + .into_iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + let (sizes, cumulated, is_dynamic) = + get_field_size_from_type_clause(db, &m.type_clause(db)); + + cumulated_sizes += cumulated; + is_dynamic_size |= is_dynamic; + Some(sizes) + }) + .flatten() + .collect::>(); + build_size_function_body(&mut sizes, cumulated_sizes, is_dynamic_size, is_packed) +} + +pub fn compute_enum_variant_sizes( + db: &dyn SyntaxGroup, + enum_ast: &ItemEnum, +) -> Vec<(Vec, u32, bool)> { + enum_ast + .variants(db) + .elements(db) + .iter() + .map(|v| match v.type_clause(db) { + OptionTypeClause::Empty(_) => (vec![], 0, false), + OptionTypeClause::TypeClause(type_clause) => { + get_field_size_from_type_clause(db, &type_clause) + } + }) + .collect::>() +} + +pub fn is_enum_packable(variant_sizes: &[(Vec, u32, bool)]) -> bool { + if variant_sizes.is_empty() { + return true; + } + + let v0_sizes = variant_sizes[0].0.clone(); + let v0_fixed_size = variant_sizes[0].1; + + variant_sizes.iter().all(|vs| { + vs.0.len() == v0_sizes.len() + && vs.0.iter().zip(v0_sizes.iter()).all(|(a, b)| a == b) + && vs.1 == v0_fixed_size + && !vs.2 + }) +} + +pub fn compute_enum_layout_size( + variant_sizes: &[(Vec, u32, bool)], + is_packed: bool, +) -> String { + if variant_sizes.is_empty() { + return "Option::None".to_string(); + } + + let v0 = variant_sizes[0].clone(); + let identical_variants = + variant_sizes.iter().all(|vs| vs.0 == v0.0 && vs.1 == v0.1 && vs.2 == v0.2); + + if identical_variants { + let (mut sizes, mut cumulated_sizes, is_dynamic_size) = v0; + + // add one felt252 to store the variant identifier + cumulated_sizes += 1; + + build_size_function_body(&mut sizes, cumulated_sizes, is_dynamic_size, is_packed) + } else { + "Option::None".to_string() + } +} + +pub fn build_size_function_body( + sizes: &mut Vec, + cumulated_sizes: u32, + is_dynamic_size: bool, + is_packed: bool, +) -> String { + if is_dynamic_size { + return "Option::None".to_string(); + } + + if cumulated_sizes > 0 { + sizes.push(format!("Option::Some({})", cumulated_sizes)); + } + + match sizes.len() { + 0 => "Option::None".to_string(), + 1 => sizes[0].clone(), + _ => { + let none_check = if is_packed { + "" + } else { + "if dojo::utils::any_none(@sizes) { + return Option::None; + }" + }; + + format!( + "let sizes : Array> = array![ + {} + ]; + + {none_check} + Option::Some(dojo::utils::sum(sizes)) + ", + sizes.join(",\n") + ) + } + } +} + +pub fn get_field_size_from_type_clause( + db: &dyn SyntaxGroup, + type_clause: &TypeClause, +) -> (Vec, u32, bool) { + let mut cumulated_sizes = 0; + let mut is_dynamic_size = false; + + let field_sizes = match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db).trim().to_string(); + compute_item_size_from_type(&path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db).trim().to_string(); + compute_tuple_size_from_type(&tuple_type) + } + _ => { + // field type already checked while building the layout + vec!["ERROR".to_string()] + } + }; + + let sizes = field_sizes + .into_iter() + .filter_map(|s| match s.parse::() { + Ok(v) => { + cumulated_sizes += v; + None + } + Err(_) => { + if s.eq("Option::None") { + is_dynamic_size = true; + None + } else { + Some(s) + } + } + }) + .collect::>(); + + (sizes, cumulated_sizes, is_dynamic_size) +} + +pub fn compute_item_size_from_type(item_type: &String) -> Vec { + if is_array(item_type) || is_byte_array(item_type) { + vec!["Option::None".to_string()] + } else if is_tuple(item_type) { + compute_tuple_size_from_type(item_type) + } else { + let primitives = primitive_type_introspection(); + + if let Some(p) = primitives.get(item_type) { + vec![p.0.to_string()] + } else { + vec![format!("dojo::meta::introspect::Introspect::<{}>::size()", item_type)] + } + } +} + +pub fn compute_tuple_size_from_type(tuple_type: &str) -> Vec { + get_tuple_item_types(tuple_type) + .iter() + .flat_map(compute_item_size_from_type) + .collect::>() +} diff --git a/crates/dojo/macros/src/derives/introspect/ty.rs b/crates/dojo/macros/src/derives/introspect/ty.rs new file mode 100644 index 0000000000..d9e9e40a11 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/ty.rs @@ -0,0 +1,133 @@ +use cairo_lang_syntax::node::ast::{ + Expr, ItemEnum, ItemStruct, Member, OptionTypeClause, TypeClause, Variant, +}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; + +use super::utils::{get_array_item_type, get_tuple_item_types, is_array, is_byte_array, is_tuple}; + +pub fn build_struct_ty(db: &dyn SyntaxGroup, name: &String, struct_ast: &ItemStruct) -> String { + let members_ty = struct_ast + .members(db) + .elements(db) + .iter() + .map(|m| build_member_ty(db, m)) + .collect::>(); + + format!( + "dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct {{ + name: '{name}', + attrs: array![].span(), + children: array![ + {}\n + ].span() + }} + )", + members_ty.join(",\n") + ) +} + +pub fn build_enum_ty(db: &dyn SyntaxGroup, name: &String, enum_ast: &ItemEnum) -> String { + let variants = enum_ast.variants(db).elements(db); + + let variants_ty = if variants.is_empty() { + "".to_string() + } else { + variants.iter().map(|v| build_variant_ty(db, v)).collect::>().join(",\n") + }; + + format!( + "dojo::meta::introspect::Ty::Enum( + dojo::meta::introspect::Enum {{ + name: '{name}', + attrs: array![].span(), + children: array![ + {variants_ty}\n + ].span() + }} + )" + ) +} + +pub fn build_member_ty(db: &dyn SyntaxGroup, member: &Member) -> String { + let name = member.name(db).text(db).to_string(); + let attrs = if member.has_attr(db, "key") { vec!["'key'"] } else { vec![] }; + + format!( + "dojo::meta::introspect::Member {{ + name: '{name}', + attrs: array![{}].span(), + ty: {} + }}", + attrs.join(","), + build_ty_from_type_clause(db, &member.type_clause(db)) + ) +} + +pub fn build_variant_ty(db: &dyn SyntaxGroup, variant: &Variant) -> String { + let name = variant.name(db).text(db).to_string(); + match variant.type_clause(db) { + OptionTypeClause::Empty(_) => { + // use an empty tuple if the variant has no data + format!("('{name}', dojo::meta::introspect::Ty::Tuple(array![].span()))") + } + OptionTypeClause::TypeClause(type_clause) => { + format!("('{name}', {})", build_ty_from_type_clause(db, &type_clause)) + } + } +} + +pub fn build_ty_from_type_clause(db: &dyn SyntaxGroup, type_clause: &TypeClause) -> String { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db).trim().to_string(); + build_item_ty_from_type(&path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db).trim().to_string(); + build_tuple_ty_from_type(&tuple_type) + } + _ => { + // diagnostic message already handled in layout building + "ERROR".to_string() + } + } +} + +pub fn build_item_ty_from_type(item_type: &String) -> String { + if is_array(item_type) { + let array_item_type = get_array_item_type(item_type); + format!( + "dojo::meta::introspect::Ty::Array( + array![ + {} + ].span() + )", + build_item_ty_from_type(&array_item_type) + ) + } else if is_byte_array(item_type) { + "dojo::meta::introspect::Ty::ByteArray".to_string() + } else if is_tuple(item_type) { + build_tuple_ty_from_type(item_type) + } else { + format!("dojo::meta::introspect::Introspect::<{}>::ty()", item_type) + } +} + +pub fn build_tuple_ty_from_type(item_type: &str) -> String { + let tuple_items = get_tuple_item_types(item_type) + .iter() + .map(build_item_ty_from_type) + .collect::>() + .join(",\n"); + format!( + "dojo::meta::introspect::Ty::Tuple( + array![ + {} + ].span() + )", + tuple_items + ) +} diff --git a/crates/dojo/macros/src/derives/introspect/utils.rs b/crates/dojo/macros/src/derives/introspect/utils.rs new file mode 100644 index 0000000000..f57f6b6335 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/utils.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; + +#[derive(Clone, Default, Debug)] +pub struct TypeIntrospection(pub usize, pub Vec); + +// Provides type introspection information for primitive types +pub fn primitive_type_introspection() -> HashMap { + HashMap::from([ + ("bytes31".into(), TypeIntrospection(1, vec![248])), + ("felt252".into(), TypeIntrospection(1, vec![251])), + ("bool".into(), TypeIntrospection(1, vec![1])), + ("u8".into(), TypeIntrospection(1, vec![8])), + ("u16".into(), TypeIntrospection(1, vec![16])), + ("u32".into(), TypeIntrospection(1, vec![32])), + ("u64".into(), TypeIntrospection(1, vec![64])), + ("u128".into(), TypeIntrospection(1, vec![128])), + ("u256".into(), TypeIntrospection(2, vec![128, 128])), + ("usize".into(), TypeIntrospection(1, vec![32])), + ("ContractAddress".into(), TypeIntrospection(1, vec![251])), + ("ClassHash".into(), TypeIntrospection(1, vec![251])), + ]) +} + +/// Check if the provided type is an unsupported `Option`, +/// because tuples are not supported with Option. +pub fn is_unsupported_option_type(ty: &str) -> bool { + ty.starts_with("Option<(") +} + +pub fn is_byte_array(ty: &str) -> bool { + ty.eq("ByteArray") +} + +pub fn is_array(ty: &str) -> bool { + ty.starts_with("Array<") || ty.starts_with("Span<") +} + +pub fn is_tuple(ty: &str) -> bool { + ty.starts_with('(') +} + +pub fn get_array_item_type(ty: &str) -> String { + if ty.starts_with("Array<") { + ty.trim().strip_prefix("Array<").unwrap().strip_suffix('>').unwrap().to_string() + } else { + ty.trim().strip_prefix("Span<").unwrap().strip_suffix('>').unwrap().to_string() + } +} + +/// split a tuple in array of items (nested tuples are not splitted). +/// example (u8, (u16, u32), u128) -> ["u8", "(u16, u32)", "u128"] +pub fn get_tuple_item_types(ty: &str) -> Vec { + let tuple_str = ty + .trim() + .strip_prefix('(') + .unwrap() + .strip_suffix(')') + .unwrap() + .to_string() + .replace(' ', ""); + let mut items = vec![]; + let mut current_item = "".to_string(); + let mut level = 0; + + for c in tuple_str.chars() { + if c == ',' { + if level > 0 { + current_item.push(c); + } + + if level == 0 && !current_item.is_empty() { + items.push(current_item); + current_item = "".to_string(); + } + } else { + current_item.push(c); + + if c == '(' { + level += 1; + } + if c == ')' { + level -= 1; + } + } + } + + if !current_item.is_empty() { + items.push(current_item); + } + + items +} + +#[test] +pub fn test_get_tuple_item_types() { + pub fn assert_array(got: Vec, expected: Vec) { + pub fn format_array(arr: Vec) -> String { + format!("[{}]", arr.join(", ")) + } + + assert!( + got.len() == expected.len(), + "arrays have not the same length (got: {}, expected: {})", + format_array(got), + format_array(expected) + ); + + for i in 0..got.len() { + assert!( + got[i] == expected[i], + "unexpected array item: (got: {} expected: {})", + got[i], + expected[i] + ) + } + } + + let test_cases = vec![ + ("(u8,)", vec!["u8"]), + ("(u8, u16, u32)", vec!["u8", "u16", "u32"]), + ("(u8, (u16,), u32)", vec!["u8", "(u16,)", "u32"]), + ("(u8, (u16, (u8, u16)))", vec!["u8", "(u16,(u8,u16))"]), + ("(Array<(Points, Damage)>, ((u16,),)))", vec!["Array<(Points,Damage)>", "((u16,),))"]), + ( + "(u8, (u16, (u8, u16), Array<(Points, Damage)>), ((u16,),)))", + vec!["u8", "(u16,(u8,u16),Array<(Points,Damage)>)", "((u16,),))"], + ), + ]; + + for (value, expected) in test_cases { + assert_array( + get_tuple_item_types(value), + expected.iter().map(|x| x.to_string()).collect::>(), + ) + } +} diff --git a/crates/dojo/macros/src/derives/mod.rs b/crates/dojo/macros/src/derives/mod.rs new file mode 100644 index 0000000000..209dd6118b --- /dev/null +++ b/crates/dojo/macros/src/derives/mod.rs @@ -0,0 +1,230 @@ +//! Derive macros. +//! +//! A derive macros is a macro that is used to generate code generally for a struct or enum. +//! The input of the macro consists of the AST of the struct or enum and the attributes of the +//! derive macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{derive_macro, Diagnostic, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::attribute::structured::{AttributeArgVariant, AttributeStructurize}; +use cairo_lang_syntax::node::ast::Attribute; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::kind::SyntaxKind::{ItemEnum, ItemStruct}; +use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; +use introspect::{handle_introspect_enum, handle_introspect_struct}; +use print::{handle_print_enum, handle_print_struct}; + +use crate::diagnostic_ext::DiagnosticsExt; + +pub mod introspect; +pub mod print; + +pub const DOJO_PRINT_DERIVE: &str = "Print"; +pub const DOJO_INTROSPECT_DERIVE: &str = "Introspect"; +pub const DOJO_PACKED_DERIVE: &str = "IntrospectPacked"; + +#[derive_macro] +fn introspect(token_stream: TokenStream) -> ProcMacroResult { + handle_derives_macros(token_stream) +} + +#[derive_macro] +fn introspect_packed(token_stream: TokenStream) -> ProcMacroResult { + handle_derives_macros(token_stream) +} + +fn handle_derives_macros(token_stream: TokenStream) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (syn_file, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in syn_file.descendants(&db) { + // Process only the first module expected to be the contract. + return match n.kind(&db) { + ItemStruct => { + let struct_ast = ast::ItemStruct::from_syntax_node(&db, n); + let attrs = struct_ast.attributes(&db).query_attr(&db, "derive"); + + dojo_derive_all(&db, attrs, &ast::ModuleItem::Struct(struct_ast)) + } + ItemEnum => { + let enum_ast = ast::ItemEnum::from_syntax_node(&db, n); + let attrs = enum_ast.attributes(&db).query_attr(&db, "derive"); + + dojo_derive_all(&db, attrs, &ast::ModuleItem::Enum(enum_ast)) + } + _ => { + continue; + } + }; + } + + ProcMacroResult::new(TokenStream::empty()) +} + +/// Handles all the dojo derives macro and returns the generated code and diagnostics. +pub fn dojo_derive_all( + db: &dyn SyntaxGroup, + attrs: Vec, + item_ast: &ast::ModuleItem, +) -> ProcMacroResult { + if attrs.is_empty() { + return ProcMacroResult::new(TokenStream::empty()); + } + + let mut diagnostics = vec![]; + + let derive_attr_names = extract_derive_attr_names(db, &mut diagnostics, attrs); + + let (rewrite_nodes, derive_diagnostics) = handle_derive_attrs(db, &derive_attr_names, item_ast); + + diagnostics.extend(derive_diagnostics); + + let mut builder = PatchBuilder::new(db, item_ast); + for node in rewrite_nodes { + builder.add_modified(node); + } + + let (code, _) = builder.build(); + + let item_name = item_ast.as_syntax_node().get_text_without_trivia(db).to_string(); + + crate::debug_expand(&format!("DERIVE {}", item_name), &code.to_string()); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} + +/// Handles the derive attributes of a struct or enum. +pub fn handle_derive_attrs( + db: &dyn SyntaxGroup, + attrs: &[String], + item_ast: &ast::ModuleItem, +) -> (Vec, Vec) { + let mut rewrite_nodes = Vec::new(); + let mut diagnostics = Vec::new(); + + check_for_derive_attr_conflicts(&mut diagnostics, attrs); + + match item_ast { + ast::ModuleItem::Struct(struct_ast) => { + for a in attrs { + match a.as_str() { + DOJO_PRINT_DERIVE => { + rewrite_nodes.push(handle_print_struct(db, struct_ast.clone())); + } + DOJO_INTROSPECT_DERIVE => { + rewrite_nodes.push(handle_introspect_struct( + db, + &mut diagnostics, + struct_ast.clone(), + false, + )); + } + DOJO_PACKED_DERIVE => { + rewrite_nodes.push(handle_introspect_struct( + db, + &mut diagnostics, + struct_ast.clone(), + true, + )); + } + _ => continue, + } + } + } + ast::ModuleItem::Enum(enum_ast) => { + for a in attrs { + match a.as_str() { + DOJO_PRINT_DERIVE => { + rewrite_nodes.push(handle_print_enum(db, enum_ast.clone())); + } + DOJO_INTROSPECT_DERIVE => { + rewrite_nodes.push(handle_introspect_enum( + db, + &mut diagnostics, + enum_ast.clone(), + false, + )); + } + DOJO_PACKED_DERIVE => { + rewrite_nodes.push(handle_introspect_enum( + db, + &mut diagnostics, + enum_ast.clone(), + true, + )); + } + _ => continue, + } + } + } + _ => { + // Currently Dojo plugin doesn't support derive macros on other items than struct and + // enum. + diagnostics.push_error( + "Dojo plugin doesn't support derive macros on other items than struct and enum." + .to_string(), + ); + } + } + + (rewrite_nodes, diagnostics) +} + +/// Extracts the names of the derive attributes from the given attributes. +/// +/// # Examples +/// +/// Derive usage should look like this: +/// +/// ```no_run,ignore +/// #[derive(Introspect)] +/// struct MyStruct {} +/// ``` +/// +/// And this function will return `["Introspect"]`. +pub fn extract_derive_attr_names( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + attrs: Vec, +) -> Vec { + attrs + .iter() + .filter_map(|attr| { + let args = attr.clone().structurize(db).args; + if args.is_empty() { + diagnostics.push_error("Expected args.".to_string()); + None + } else { + Some(args.into_iter().filter_map(|a| { + if let AttributeArgVariant::Unnamed(ast::Expr::Path(path)) = a.variant { + if let [ast::PathSegment::Simple(segment)] = &path.elements(db)[..] { + Some(segment.ident(db).text(db).to_string()) + } else { + None + } + } else { + None + } + })) + } + }) + .flatten() + .collect::>() +} + +/// Checks for conflicts between introspect and packed attributes. +/// +/// Introspect and IntrospectPacked cannot be used at a same time. +fn check_for_derive_attr_conflicts(diagnostics: &mut Vec, attr_names: &[String]) { + if attr_names.contains(&DOJO_INTROSPECT_DERIVE.to_string()) + && attr_names.contains(&DOJO_PACKED_DERIVE.to_string()) + { + diagnostics.push_error(format!( + "{} and {} attributes cannot be used at a same time.", + DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE + )); + } +} diff --git a/crates/dojo/macros/src/derives/print.rs b/crates/dojo/macros/src/derives/print.rs new file mode 100644 index 0000000000..999adf2622 --- /dev/null +++ b/crates/dojo/macros/src/derives/print.rs @@ -0,0 +1,96 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_syntax::node::ast::{ItemEnum, ItemStruct, OptionTypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +/// Derives PrintTrait for a struct. +/// Parameters: +/// * db: The semantic database. +/// * struct_ast: The AST of the model struct. +/// +/// Returns: +/// * A RewriteNode containing the generated code. +pub fn handle_print_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> RewriteNode { + let prints: Vec<_> = struct_ast + .members(db) + .elements(db) + .iter() + .map(|m| { + format!( + "core::debug::PrintTrait::print('{}'); core::debug::PrintTrait::print(self.{});", + m.name(db).text(db).to_string(), + m.name(db).text(db).to_string() + ) + }) + .collect(); + + RewriteNode::interpolate_patched( + " +#[cfg(test)] +impl $type_name$StructPrintImpl of core::debug::PrintTrait<$type_name$> { + fn print(self: $type_name$) { + $print$ + } +} +", + &UnorderedHashMap::from([ + ( + "type_name".to_string(), + RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node()), + ), + ("print".to_string(), RewriteNode::Text(prints.join("\n"))), + ]), + ) +} + +/// Derives PrintTrait for an enum. +/// Parameters: +/// * db: The semantic database. +/// * enum_ast: The AST of the model enum. +/// +/// Returns: +/// * A RewriteNode containing the generated code. +pub fn handle_print_enum(db: &dyn SyntaxGroup, enum_ast: ItemEnum) -> RewriteNode { + let enum_name = enum_ast.name(db).text(db); + let prints: Vec<_> = enum_ast + .variants(db) + .elements(db) + .iter() + .map(|m| { + let variant_name = m.name(db).text(db).to_string(); + match m.type_clause(db) { + OptionTypeClause::Empty(_) => { + format!( + "{enum_name}::{variant_name} => {{ \ + core::debug::PrintTrait::print('{variant_name}'); }}" + ) + } + OptionTypeClause::TypeClause(_) => { + format!( + "{enum_name}::{variant_name}(v) => {{ \ + core::debug::PrintTrait::print('{variant_name}'); \ + core::debug::PrintTrait::print(v); }}" + ) + } + } + }) + .collect(); + + RewriteNode::interpolate_patched( + " +#[cfg(test)] +impl $type_name$EnumPrintImpl of core::debug::PrintTrait<$type_name$> { + fn print(self: $type_name$) { + match self { + $print$ + } + } +} +", + &UnorderedHashMap::from([ + ("type_name".to_string(), RewriteNode::new_trimmed(enum_ast.name(db).as_syntax_node())), + ("print".to_string(), RewriteNode::Text(prints.join(",\n"))), + ]), + ) +} diff --git a/crates/dojo/macros/src/diagnostic_ext.rs b/crates/dojo/macros/src/diagnostic_ext.rs new file mode 100644 index 0000000000..05b4d0032e --- /dev/null +++ b/crates/dojo/macros/src/diagnostic_ext.rs @@ -0,0 +1,16 @@ +use cairo_lang_macro::Diagnostic; + +pub trait DiagnosticsExt { + fn push_error(&mut self, message: String); + fn push_warn(&mut self, message: String); +} + +impl DiagnosticsExt for Vec { + fn push_error(&mut self, message: String) { + self.push(Diagnostic::error(message)); + } + + fn push_warn(&mut self, message: String) { + self.push(Diagnostic::warn(message)); + } +} diff --git a/crates/dojo/macros/src/lib.rs b/crates/dojo/macros/src/lib.rs new file mode 100644 index 0000000000..d61053815b --- /dev/null +++ b/crates/dojo/macros/src/lib.rs @@ -0,0 +1,19 @@ +pub mod attributes; +pub mod derives; +pub mod diagnostic_ext; + +#[cfg(test)] +pub mod tests; + +/// Prints the given string only if the `DOJO_EXPAND` environment variable is set. +/// This is useful for debugging the compiler with verbose output. +/// +/// # Arguments +/// +/// * `loc` - The location of the code to be expanded. +/// * `code` - The code to be expanded. +pub fn debug_expand(loc: &str, code: &str) { + if std::env::var("DOJO_EXPAND").is_ok() { + println!("\n// *> EXPAND {} <*\n{}\n\n", loc, code); + } +} diff --git a/crates/dojo/macros/src/tests/attributes/dojo_model.rs b/crates/dojo/macros/src/tests/attributes/dojo_model.rs new file mode 100644 index 0000000000..c054c32b9c --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/dojo_model.rs @@ -0,0 +1,67 @@ +use cairo_lang_macro::TokenStream; + +use crate::attributes::constants::{DOJO_EVENT_ATTR, DOJO_MODEL_ATTR}; +use crate::attributes::dojo_model::handle_model_attribute_macro; + +const SIMPLE_MODEL: &str = " +#[derive(Introspect, Drop, Serde)] +struct SimpleModel { + #[key] + k: u32, + v: u32 +}"; + +#[test] +fn test_no_struct() { + let input = TokenStream::new("enum MyEnum { X, Y }".to_string()); + + let res = handle_model_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert!(res.token_stream.is_empty()); +} + +#[test] +fn test_duplicated_attributes() { + let input = TokenStream::new(format!( + " + #[{DOJO_MODEL_ATTR}] + {SIMPLE_MODEL} + " + )); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Only one {DOJO_MODEL_ATTR} attribute is allowed per module.") + ); +} + +#[test] +fn test_attribute_conflict() { + let input = TokenStream::new(format!( + " + #[{DOJO_EVENT_ATTR}] + {SIMPLE_MODEL} + " + )); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("A {DOJO_MODEL_ATTR} can't be used together with a {DOJO_EVENT_ATTR}.") + ); +} + +#[test] +fn test_simple_model() { + let input = TokenStream::new(SIMPLE_MODEL.to_string()); + + let res = handle_model_attribute_macro(input); + + println!("diagnostics: {:#?}", res.diagnostics); + + assert!(res.diagnostics.is_empty()); +} diff --git a/crates/dojo/macros/src/tests/mod.rs b/crates/dojo/macros/src/tests/mod.rs new file mode 100644 index 0000000000..682377b0cb --- /dev/null +++ b/crates/dojo/macros/src/tests/mod.rs @@ -0,0 +1,3 @@ +mod attributes { + mod dojo_model; +} diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index 0d453bfdcb..ca8cf36aa8 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -5,16 +5,16 @@ version = 1 name = "dojo" version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" [[package]] name = "types_test" -version = "1.0.0" +version = "1.0.1" dependencies = [ "dojo", ] diff --git a/crates/torii/types-test/src/contracts.cairo b/crates/torii/types-test/src/contracts.cairo index 3a114d71cb..fdc779c4f2 100644 --- a/crates/torii/types-test/src/contracts.cairo +++ b/crates/torii/types-test/src/contracts.cairo @@ -4,7 +4,7 @@ trait IRecords { fn delete(ref self: T, record_id: u32); } -#[dojo::contract] +#[dojo_contract] mod records { use types_test::models::{ Record, RecordSibling, Subrecord, Nested, NestedMore, NestedMost, Depth @@ -16,7 +16,7 @@ mod records { use super::IRecords; #[derive(Drop, Serde, starknet::Event)] - #[dojo::event] + #[dojo_event] struct RecordLogged { #[key] record_id: u32, diff --git a/crates/torii/types-test/src/models.cairo b/crates/torii/types-test/src/models.cairo index a1d9cad0ea..aa35da1230 100644 --- a/crates/torii/types-test/src/models.cairo +++ b/crates/torii/types-test/src/models.cairo @@ -1,7 +1,7 @@ use starknet::{ContractAddress, ClassHash}; #[derive(Introspect, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Record { #[key] pub record_id: u32, @@ -30,7 +30,7 @@ pub struct Record { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct RecordSibling { #[key] pub record_id: u32, @@ -61,7 +61,7 @@ pub struct NestedMost { } #[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Subrecord { #[key] pub record_id: u32, @@ -91,7 +91,7 @@ impl DepthIntoFelt252 of Into { } // takes a long time to deploy, uncomment for now // #[derive(Introspect, Copy, Drop, Serde)] -// #[dojo::model] +// #[dojo_model] // struct FatModel { // #[key] // record_id: u32, diff --git a/examples/game-lib/Scarb.lock b/examples/game-lib/Scarb.lock index 775fb46b9b..10275b4aa2 100644 --- a/examples/game-lib/Scarb.lock +++ b/examples/game-lib/Scarb.lock @@ -17,11 +17,11 @@ dependencies = [ [[package]] name = "dojo" -version = "1.0.0-rc.0" +version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/examples/game-lib/armory/src/lib.cairo b/examples/game-lib/armory/src/lib.cairo index b182078d8a..e709e8dcd4 100644 --- a/examples/game-lib/armory/src/lib.cairo +++ b/examples/game-lib/armory/src/lib.cairo @@ -1,5 +1,5 @@ #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Flatbow { #[key] pub id: u32, diff --git a/examples/game-lib/bestiary/src/lib.cairo b/examples/game-lib/bestiary/src/lib.cairo index 77f4ac831c..f3d395b9af 100644 --- a/examples/game-lib/bestiary/src/lib.cairo +++ b/examples/game-lib/bestiary/src/lib.cairo @@ -1,5 +1,5 @@ #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct RiverSkale { #[key] pub id: u32, diff --git a/examples/simple/Scarb.lock b/examples/simple/Scarb.lock index 4067faed93..ee5d82cbd7 100644 --- a/examples/simple/Scarb.lock +++ b/examples/simple/Scarb.lock @@ -3,9 +3,9 @@ version = 1 [[package]] name = "dojo" -version = "1.0.0" +version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] @@ -16,8 +16,8 @@ dependencies = [ ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" [[package]] name = "dojo_simple" diff --git a/examples/simple/src/lib.cairo b/examples/simple/src/lib.cairo index 682d9039c1..ed6c45f648 100644 --- a/examples/simple/src/lib.cairo +++ b/examples/simple/src/lib.cairo @@ -5,7 +5,7 @@ pub mod sn_c1 { } #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct M { #[key] pub k: felt252, @@ -13,7 +13,7 @@ pub struct M { } #[derive(Introspect, Drop, Serde)] -#[dojo::event] +#[dojo_event] pub struct E { #[key] pub k: felt252, @@ -21,7 +21,7 @@ pub struct E { } #[derive(Introspect, Drop, Serde)] -#[dojo::event] +#[dojo_event] pub struct EH { #[key] pub k: felt252, @@ -36,7 +36,7 @@ pub trait MyInterface { fn system_4(ref self: T, k: felt252); } -#[dojo::contract] +#[dojo_contract] pub mod c1 { use super::{MyInterface, M, E, EH, MValue}; use dojo::model::{ModelStorage, ModelValueStorage, Model}; @@ -106,10 +106,10 @@ pub mod c1 { } } -#[dojo::contract] +#[dojo_contract] pub mod c2 {} -#[dojo::contract] +#[dojo_contract] pub mod c3 {} #[cfg(test)] diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index a3ce4d9320..d6e2d4a4f6 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -19,7 +19,7 @@ dependencies = [ name = "dojo" version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "dojo_examples" -version = "1.0.0" +version = "1.0.1" dependencies = [ "armory", "bestiary", @@ -40,5 +40,5 @@ dependencies = [ ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 1a8071ada3..5d7b33413a 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -13,7 +13,7 @@ pub trait IActions { fn enter_dungeon(ref self: T, dungeon_address: starknet::ContractAddress); } -#[dojo::contract] +#[dojo_contract] pub mod actions { use super::IActions; @@ -35,7 +35,7 @@ pub mod actions { use bestiary::RiverSkale; #[derive(Copy, Drop, Serde)] - #[dojo::event] + #[dojo_event] pub struct Moved { #[key] pub player: ContractAddress, diff --git a/examples/spawn-and-move/src/dungeon.cairo b/examples/spawn-and-move/src/dungeon.cairo index 8ac2950b7f..e7f058fe2a 100644 --- a/examples/spawn-and-move/src/dungeon.cairo +++ b/examples/spawn-and-move/src/dungeon.cairo @@ -3,7 +3,7 @@ pub trait IDungeon { fn enter(self: @T); } -#[dojo::contract] +#[dojo_contract] pub mod dungeon { #[abi(embed_v0)] pub impl IDungeonImpl of super::IDungeon { diff --git a/examples/spawn-and-move/src/mock_token.cairo b/examples/spawn-and-move/src/mock_token.cairo index a7a637038a..ab2e27aa41 100644 --- a/examples/spawn-and-move/src/mock_token.cairo +++ b/examples/spawn-and-move/src/mock_token.cairo @@ -1,4 +1,4 @@ -#[dojo::contract] +#[dojo_contract] pub mod mock_token { use dojo_examples::models::{MockToken}; use dojo::model::ModelStorage; diff --git a/examples/spawn-and-move/src/models.cairo b/examples/spawn-and-move/src/models.cairo index fc389d2cc9..2a38e94bf5 100644 --- a/examples/spawn-and-move/src/models.cairo +++ b/examples/spawn-and-move/src/models.cairo @@ -22,7 +22,7 @@ impl DirectionIntoFelt252 of Into { } #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct Message { #[key] pub identity: ContractAddress, @@ -34,7 +34,7 @@ pub struct Message { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] pub struct Moves { #[key] pub player: ContractAddress, @@ -43,7 +43,7 @@ pub struct Moves { } #[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] +#[dojo_model] pub struct MockToken { #[key] pub account: ContractAddress, @@ -61,7 +61,7 @@ pub struct Vec2 { // Any field that is a custom type into a `IntrospectPacked` type // must be packed. #[derive(Copy, Drop, Serde, IntrospectPacked, Debug)] -#[dojo::model] +#[dojo_model] pub struct Position { #[key] pub player: ContractAddress, @@ -78,7 +78,7 @@ pub struct PlayerItem { } #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct PlayerConfig { #[key] pub player: ContractAddress, @@ -88,7 +88,7 @@ pub struct PlayerConfig { } #[derive(Drop, Serde)] -#[dojo::model] +#[dojo_model] pub struct ServerProfile { #[key] pub player: ContractAddress, diff --git a/examples/spawn-and-move/src/others.cairo b/examples/spawn-and-move/src/others.cairo index abbfa42676..4b60c15357 100644 --- a/examples/spawn-and-move/src/others.cairo +++ b/examples/spawn-and-move/src/others.cairo @@ -1,10 +1,10 @@ -#[dojo::contract] +#[dojo_contract] pub mod others { use starknet::{ContractAddress, get_caller_address}; use dojo::event::EventStorage; #[derive(Copy, Drop, Serde)] - #[dojo::event] + #[dojo_event] struct MyInit { #[key] caller: ContractAddress,