Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add #[dojo::event] attribute #1721

Merged
merged 16 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/dojo-core/src/base_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ mod invalid_model {
impl InvalidModelName of super::INameOnly<ContractState> {
fn name(self: @ContractState) -> felt252 {
// Pre-computed address of a contract deployed through the world.
0x7b6cc67bb03efdf091487465df2037cad74111d8b616536b013e70da7491a30
// To print this addres, run:
// sozo test --manifest-path crates/dojo-core/Scarb.toml -f test_deploy_from_world_invalid_model
0x647d90f9663c37478a5fba689fc7166d957f782ea4a8316e0042929d48cf8be
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/dojo-core/src/world.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ mod world {
fn emit(self: @ContractState, mut keys: Array<felt252>, values: Span<felt252>) {
let system = get_caller_address();
system.serialize(ref keys);

emit_event_syscall(keys.span(), values).unwrap_syscall();
}

Expand Down
1 change: 1 addition & 0 deletions crates/dojo-lang/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ impl DojoContract {
name,
dependencies: system.dependencies.values().cloned().collect(),
}],
events: vec![],
})),
code_mappings: builder.code_mappings,
}),
Expand Down
190 changes: 190 additions & 0 deletions crates/dojo-lang/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use cairo_lang_defs::patcher::{ModifiedNode, RewriteNode};
use cairo_lang_defs::plugin::PluginDiagnostic;
use cairo_lang_starknet::plugin::aux_data::StarkNetEventAuxData;
use cairo_lang_starknet::plugin::consts::{
EVENT_TRAIT, EVENT_TYPE_NAME, KEY_ATTR, NESTED_ATTR, SERDE_ATTR,
};
use cairo_lang_starknet::plugin::events::{EventData, EventFieldKind};
use cairo_lang_syntax::node::db::SyntaxGroup;
use cairo_lang_syntax::node::helpers::QueryAttrs;
use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode};
use indoc::formatdoc;

use crate::plugin::DojoAuxData;

// A custom implementation of the starknet::Event derivation path.
// We append the event selector directly within the append_keys_and_data function.
// Without the need of the enum for all event variants.

// https://github.com/starkware-libs/cairo/blob/main/crates/cairo-lang-starknet/src/plugin/derive/event.rs

pub fn handle_event_struct(
db: &dyn SyntaxGroup,
aux_data: &mut DojoAuxData,
struct_ast: ast::ItemStruct,
) -> (RewriteNode, Vec<PluginDiagnostic>) {
let mut diagnostics = vec![];

// TODO(spapini): Support generics.
let generic_params = struct_ast.generic_params(db);
match generic_params {
ast::OptionWrappedGenericParamList::Empty(_) => {}
_ => {
diagnostics.push(PluginDiagnostic::error(
generic_params.stable_ptr().untyped(),
format!("{EVENT_TYPE_NAME} structs with generic arguments are unsupported"),
));
}

Check warning on line 37 in crates/dojo-lang/src/event.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/event.rs#L32-L37

Added lines #L32 - L37 were not covered by tests
}

// Generate append_keys_and_data() code.
let mut append_members = vec![];
let mut deserialize_members = vec![];
let mut ctor = vec![];
let mut members = vec![];
for member in struct_ast.members(db).elements(db) {
let member_name = RewriteNode::new_trimmed(member.name(db).as_syntax_node());
let member_kind =
get_field_kind_for_member(db, &mut diagnostics, &member, EventFieldKind::DataSerde);
members.push((member.name(db).text(db), member_kind));

let member_for_append = RewriteNode::interpolate_patched(
"self.$member_name$",
&[("member_name".to_string(), member_name.clone())].into(),
);
let append_member = append_field(member_kind, member_for_append);
let deserialize_member = deserialize_field(member_kind, member_name.clone());
append_members.push(append_member);
deserialize_members.push(deserialize_member);
ctor.push(RewriteNode::interpolate_patched(
"$member_name$, ",
&[("member_name".to_string(), member_name)].into(),
));
}
let event_data = EventData::Struct { members };
aux_data.events.push(StarkNetEventAuxData { event_data });

let append_members = RewriteNode::Modified(ModifiedNode { children: Some(append_members) });
let deserialize_members =
RewriteNode::Modified(ModifiedNode { children: Some(deserialize_members) });
let ctor = RewriteNode::Modified(ModifiedNode { children: Some(ctor) });

// Add an implementation for `Event<StructName>`.
let struct_name = RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node());
(
// Append the event selector using the struct_name for the selector
// and then append the members.
RewriteNode::interpolate_patched(
&formatdoc!(
"
impl $struct_name$IsEvent of {EVENT_TRAIT}<$struct_name$> {{
fn append_keys_and_data(
self: @$struct_name$, ref keys: Array<felt252>, ref data: Array<felt252>
) {{
core::array::ArrayTrait::append(ref keys, selector!(\"$struct_name$\"));
$append_members$
}}
fn deserialize(
ref keys: Span<felt252>, ref data: Span<felt252>,
) -> Option<$struct_name$> {{$deserialize_members$
Option::Some($struct_name$ {{$ctor$}})
}}
}}
"
),
&[
("struct_name".to_string(), struct_name),
("append_members".to_string(), append_members),
("deserialize_members".to_string(), deserialize_members),
("ctor".to_string(), ctor),
]
.into(),
),
diagnostics,
)
}

/// Generates code to emit an event for a field
fn append_field(member_kind: EventFieldKind, field: RewriteNode) -> RewriteNode {
match member_kind {
EventFieldKind::Nested | EventFieldKind::Flat => RewriteNode::interpolate_patched(
&format!(
"
{EVENT_TRAIT}::append_keys_and_data(
$field$, ref keys, ref data
);"
),
&[("field".to_string(), field)].into(),
),

Check warning on line 118 in crates/dojo-lang/src/event.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/event.rs#L110-L118

Added lines #L110 - L118 were not covered by tests
EventFieldKind::KeySerde => RewriteNode::interpolate_patched(
"
core::serde::Serde::serialize($field$, ref keys);",
&[("field".to_string(), field)].into(),
),
EventFieldKind::DataSerde => RewriteNode::interpolate_patched(
"
core::serde::Serde::serialize($field$, ref data);",
&[("field".to_string(), field)].into(),
),
}
}

fn deserialize_field(member_kind: EventFieldKind, member_name: RewriteNode) -> RewriteNode {
RewriteNode::interpolate_patched(
match member_kind {
EventFieldKind::Nested | EventFieldKind::Flat => {
"
let $member_name$ = starknet::Event::deserialize(
ref keys, ref data
)?;"

Check warning on line 139 in crates/dojo-lang/src/event.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/event.rs#L136-L139

Added lines #L136 - L139 were not covered by tests
}
EventFieldKind::KeySerde => {
"
let $member_name$ = core::serde::Serde::deserialize(
ref keys
)?;"
}
EventFieldKind::DataSerde => {
"
let $member_name$ = core::serde::Serde::deserialize(
ref data
)?;"
}
},
&[("member_name".to_string(), member_name)].into(),
)
}

/// Retrieves the field kind for a given enum variant,
/// indicating how the field should be serialized.
/// See [EventFieldKind].
fn get_field_kind_for_member(
db: &dyn SyntaxGroup,
diagnostics: &mut Vec<PluginDiagnostic>,
member: &ast::Member,
default: EventFieldKind,
) -> EventFieldKind {
let is_nested = member.has_attr(db, NESTED_ATTR);
let is_key = member.has_attr(db, KEY_ATTR);
let is_serde = member.has_attr(db, SERDE_ATTR);

// Currently, nested fields are unsupported.
if is_nested {
diagnostics.push(PluginDiagnostic::error(
member.stable_ptr().untyped(),
"Nested event fields are currently unsupported".to_string(),
));

Check warning on line 176 in crates/dojo-lang/src/event.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/event.rs#L173-L176

Added lines #L173 - L176 were not covered by tests
}
// Currently, serde fields are unsupported.
if is_serde {
diagnostics.push(PluginDiagnostic::error(
member.stable_ptr().untyped(),
"Serde event fields are currently unsupported".to_string(),
));

Check warning on line 183 in crates/dojo-lang/src/event.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/event.rs#L180-L183

Added lines #L180 - L183 were not covered by tests
}

if is_key {
return EventFieldKind::KeySerde;
}
default
}
16 changes: 6 additions & 10 deletions crates/dojo-lang/src/inline_macros/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
};
use cairo_lang_diagnostics::Severity;
use cairo_lang_semantic::inline_macros::unsupported_bracket_diagnostic;
use cairo_lang_starknet::plugin::consts::EVENT_TRAIT;
use cairo_lang_syntax::node::{ast, TypedSyntaxNode};

use super::unsupported_arg_diagnostic;
use crate::inline_macros::unsupported_arg_diagnostic;

#[derive(Debug, Default)]
pub struct EmitMacro;
Expand All @@ -29,12 +30,12 @@

let args = arg_list.arguments(db).elements(db);

if args.len() != 2 {
if args.len() < 2 || args.len() > 3 {
return InlinePluginResult {
code: None,
diagnostics: vec![PluginDiagnostic {
stable_ptr: arg_list.arguments(db).stable_ptr().untyped(),
message: "Invalid arguments. Expected \"emit!(world, models,)\"".to_string(),
message: "Invalid arguments. Expected \"emit!(world, (events,))\"".to_string(),

Check warning on line 38 in crates/dojo-lang/src/inline_macros/emit.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/inline_macros/emit.rs#L38

Added line #L38 was not covered by tests
severity: Severity::Error,
}],
};
Expand Down Expand Up @@ -67,7 +68,7 @@
return InlinePluginResult {
code: None,
diagnostics: vec![PluginDiagnostic {
message: "Invalid arguments. Expected \"(world, (models,))\"".to_string(),
message: "Invalid arguments. Expected \"(world, (events,))\"".to_string(),

Check warning on line 71 in crates/dojo-lang/src/inline_macros/emit.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/inline_macros/emit.rs#L71

Added line #L71 was not covered by tests
stable_ptr: arg_list.arguments(db).stable_ptr().untyped(),
severity: Severity::Error,
}],
Expand Down Expand Up @@ -95,14 +96,9 @@
let mut data = Default::<core::array::Array>::default();",
);

builder.add_str(&format!(
"keys.append(selector!(\"{}\"));",
event.split_whitespace().next().unwrap()
));

builder.add_str(&format!(
"
starknet::Event::append_keys_and_data(@{event}, ref keys, ref data);",
{EVENT_TRAIT}::append_keys_and_data(@{event}, ref keys, ref data);",
event = event
));

Expand Down
1 change: 1 addition & 0 deletions crates/dojo-lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Learn more at [dojoengine.gg](http://dojoengine.gg).
pub mod compiler;
pub mod contract;
pub mod event;
pub mod inline_macros;
pub mod interface;
pub mod introspect;
Expand Down
32 changes: 30 additions & 2 deletions crates/dojo-lang/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::cmp::Ordering;

use anyhow::Result;
use cairo_lang_defs::patcher::PatchBuilder;
use cairo_lang_defs::plugin::{
Expand All @@ -6,6 +8,7 @@
};
use cairo_lang_diagnostics::Severity;
use cairo_lang_semantic::plugin::PluginSuite;
use cairo_lang_starknet::plugin::aux_data::StarkNetEventAuxData;
use cairo_lang_syntax::attribute::structured::{
AttributeArg, AttributeArgVariant, AttributeStructurize,
};
Expand All @@ -23,6 +26,7 @@
use url::Url;

use crate::contract::DojoContract;
use crate::event::handle_event_struct;
use crate::inline_macros::array_cap::ArrayCapMacro;
use crate::inline_macros::delete::DeleteMacro;
use crate::inline_macros::emit::EmitMacro;
Expand All @@ -33,8 +37,9 @@
use crate::model::handle_model_struct;
use crate::print::{handle_print_enum, handle_print_struct};

const DOJO_CONTRACT_ATTR: &str = "dojo::contract";
const DOJO_INTERFACE_ATTR: &str = "dojo::interface";
pub const DOJO_CONTRACT_ATTR: &str = "dojo::contract";
pub const DOJO_INTERFACE_ATTR: &str = "dojo::interface";
pub const DOJO_EVENT_ATTR: &str = "dojo::event";
const DOJO_PLUGIN_EXPAND_VAR_ENV: &str = "DOJO_PLUGIN_EXPAND";

#[derive(Clone, Debug, PartialEq)]
Expand All @@ -56,6 +61,8 @@
pub models: Vec<Model>,
/// A list of systems that were processed by the plugin and their model dependencies.
pub systems: Vec<SystemAuxData>,
/// A list of events that were processed by the plugin.
pub events: Vec<StarkNetEventAuxData>,
}

impl GeneratedFileAuxData for DojoAuxData {
Expand Down Expand Up @@ -406,6 +413,26 @@
}
}

let attributes = struct_ast.attributes(db).query_attr(db, DOJO_EVENT_ATTR);

match attributes.len().cmp(&1) {
Ordering::Equal => {
let (event_rewrite_nodes, event_diagnostics) =
handle_event_struct(db, &mut aux_data, struct_ast.clone());
rewrite_nodes.push(event_rewrite_nodes);
diagnostics.extend(event_diagnostics);
}
Ordering::Greater => {
diagnostics.push(PluginDiagnostic {
message: "A Dojo event must have zero or one dojo::event attribute."
.into(),
stable_ptr: struct_ast.stable_ptr().untyped(),
severity: Severity::Error,
});
}

Check warning on line 432 in crates/dojo-lang/src/plugin.rs

View check run for this annotation

Codecov / codecov/patch

crates/dojo-lang/src/plugin.rs#L425-L432

Added lines #L425 - L432 were not covered by tests
_ => {}
}

if rewrite_nodes.is_empty() {
return PluginResult { diagnostics, ..PluginResult::default() };
}
Expand Down Expand Up @@ -439,6 +466,7 @@
fn declared_attributes(&self) -> Vec<String> {
vec![
"dojo::contract".to_string(),
"dojo::event".to_string(),
"key".to_string(),
"computed".to_string(),
// Not adding capacity for now, this will automatically
Expand Down
2 changes: 1 addition & 1 deletion crates/sozo/ops/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ mod tests {
let result = extract_events(&manifest, &manifest_dir).unwrap();

// we are just collecting all events from manifest file so just verifying count should work
assert_eq!(result.len(), 12);
assert_eq!(result.len(), 11);
}

use cainome::parser::tokens::{Array, Composite, CompositeInner, CompositeType};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
kind = "DojoContract"
class_hash = "0xf6347423d9f59912d0b441646d448f5d4d30869e634061670e090266a41d0d"
class_hash = "0x658309df749cea1c32e21920740011e829626ab06c9b4d0c05b75f82a20693b"
abi = "abis/base/contracts/records.json"
reads = []
writes = []
Expand Down
Loading
Loading