diff --git a/corelib/src/test/language_features/macro_test.cairo b/corelib/src/test/language_features/macro_test.cairo index 74167f104c1..5ec23d2b98f 100644 --- a/corelib/src/test/language_features/macro_test.cairo +++ b/corelib/src/test/language_features/macro_test.cairo @@ -54,11 +54,14 @@ mod test_assert_eq { macro assert_eq { ($left:ident, $right:ident) => { if $left != $right { - panic!("PANIC!"); + // TODO(Gil): Call `panic!` directly when $callsite is supported inside plugins. + $callsite::panic(); } }; } - + fn panic() { + panic!("PANIC!"); + } #[test] #[should_panic(expected: ("PANIC!",))] fn test_user_defined_assert_eq() { @@ -284,7 +287,10 @@ mod repetition_macro_expansion { }; [$x:expr, $y:expr] => { - array![$x, $y] + let mut arr = $defsite::ArrayTrait::new(); + arr.append($x); + arr.append($y); + arr }; } @@ -325,7 +331,7 @@ fn test_count_exprs_rec() { macro my_array { [$x:expr] => { - let mut arr = array![]; + let mut arr = $defsite::ArrayTrait::new(); arr.append($x); arr }; @@ -344,3 +350,27 @@ fn test_array_macro() { let mut expected = array![3, 2, 1]; assert_eq!(result, expected); } + +mod callsite_test { + fn foo(x: felt252) -> felt252 { + x + 100 + } + + mod inner { + fn foo(x: felt252) -> felt252 { + x + 200 + } + + pub macro call_foo { + ($x:expr) => { + $callsite::foo($x) + }; + } + } + + #[test] + fn test_callsite_resolution() { + assert_eq!(inner::call_foo!(5), 105); + assert_eq!(inner::call_foo!((foo(5))), 205); + } +} diff --git a/crates/cairo-lang-semantic/src/diagnostic.rs b/crates/cairo-lang-semantic/src/diagnostic.rs index ac8e7676a3f..fbd6f51f49a 100644 --- a/crates/cairo-lang-semantic/src/diagnostic.rs +++ b/crates/cairo-lang-semantic/src/diagnostic.rs @@ -970,6 +970,11 @@ impl DiagnosticEntry for SemanticDiagnostic { SemanticDiagnosticKind::EmptyPathAfterResolverModifier => { "Empty path after `$defsite` is not allowed.".into() } + SemanticDiagnosticKind::PathInMacroWithoutModifier => { + "Path in a macro without a resolver modifier ($callsite or $defsite) - currently \ + resolving as $callsite but this will not be supported in future versions." + .into() + } SemanticDiagnosticKind::CannotCreateInstancesOfPhantomTypes => { "Can not create instances of phantom types.".into() } @@ -1099,7 +1104,8 @@ impl DiagnosticEntry for SemanticDiagnostic { | SemanticDiagnosticKind::CallingShadowedFunction { .. } | SemanticDiagnosticKind::UnusedConstant | SemanticDiagnosticKind::UnusedUse - | SemanticDiagnosticKind::UnsupportedAllowAttrArguments => Severity::Warning, + | SemanticDiagnosticKind::UnsupportedAllowAttrArguments + | SemanticDiagnosticKind::PathInMacroWithoutModifier => Severity::Warning, SemanticDiagnosticKind::PluginDiagnostic(diag) => diag.severity, _ => Severity::Error, } @@ -1471,6 +1477,7 @@ pub enum SemanticDiagnosticKind { modifier: SmolStr, }, EmptyPathAfterResolverModifier, + PathInMacroWithoutModifier, DerefCycle { deref_chain: String, }, diff --git a/crates/cairo-lang-semantic/src/expr/compute.rs b/crates/cairo-lang-semantic/src/expr/compute.rs index 815adcf9cf9..bb3648f500e 100644 --- a/crates/cairo-lang-semantic/src/expr/compute.rs +++ b/crates/cairo-lang-semantic/src/expr/compute.rs @@ -76,7 +76,7 @@ use crate::items::us::get_use_path_segments; use crate::items::visibility; use crate::resolve::{ AsSegments, EnrichedMembers, EnrichedTypeMemberAccess, ResolutionContext, ResolvedConcreteItem, - ResolvedGenericItem, Resolver, + ResolvedGenericItem, Resolver, ResolverMacroData, }; use crate::semantic::{self, Binding, FunctionId, LocalVariable, TypeId, TypeLongId}; use crate::substitution::SemanticRewriter; @@ -481,7 +481,6 @@ fn compute_expr_inline_macro_semantic( ) -> Maybe { let db = ctx.db; let macro_name = syntax.path(db).identifier(ctx.db).to_string(); - let prev_macro_resolver_data = ctx.resolver.macro_defsite_data.clone(); let crate_id = ctx.resolver.owning_crate_id; // Skipping expanding an inline macro if it had a parser error. if syntax.as_syntax_node().descendants(db).any(|node| { @@ -507,6 +506,9 @@ fn compute_expr_inline_macro_semantic( NotFoundItemType::Macro, ResolutionContext::Statement(&mut ctx.environment), ); + let inference_id = ctx.resolver.inference().inference_id; + let prev_macro_resolver_data = ctx.resolver.macro_call_data.clone(); + let callsite_resolver = ctx.resolver.data.clone_with_inference_id(ctx.db, inference_id); let (content, name, mappings, is_macro_rule) = if let Ok(ResolvedGenericItem::Macro(macro_declaration_id)) = user_defined_macro { let macro_rules = ctx.db.macro_declaration_rules(macro_declaration_id)?; @@ -523,9 +525,18 @@ fn compute_expr_inline_macro_semantic( let mut matcher_ctx = MatcherContext { captures, placeholder_to_rep_id, ..Default::default() }; let expanded_code = expand_macro_rule(ctx.db, rule, &mut matcher_ctx)?; - let macro_resolver_data = + + let macro_defsite_resolver_data = ctx.db.macro_declaration_resolver_data(macro_declaration_id)?; - ctx.resolver.macro_defsite_data = Some(macro_resolver_data); + let parent_macro_call_data = ctx.resolver.macro_call_data.clone(); + ctx.resolver.macro_call_data = Some(ResolverMacroData { + defsite_data: macro_defsite_resolver_data, + callsite_data: callsite_resolver + .clone_with_inference_id(ctx.db, inference_id) + .into(), + expansion_result: expanded_code.clone().into(), + parent_macro_call_data: parent_macro_call_data.map(|data| data.into()), + }); (expanded_code.text, macro_name.into(), expanded_code.code_mappings, true) } else if let Some(macro_plugin_id) = ctx.db.crate_inline_macro_plugins(crate_id).get(¯o_name).cloned() @@ -589,7 +600,7 @@ fn compute_expr_inline_macro_semantic( } else { compute_expr_semantic(ctx, &expr_syntax) }; - ctx.resolver.macro_defsite_data = prev_macro_resolver_data; + ctx.resolver.macro_call_data = prev_macro_resolver_data; Ok(expr.expr) } diff --git a/crates/cairo-lang-semantic/src/expr/test_data/inline_macros b/crates/cairo-lang-semantic/src/expr/test_data/inline_macros index 80dd0cb8413..da579627170 100644 --- a/crates/cairo-lang-semantic/src/expr/test_data/inline_macros +++ b/crates/cairo-lang-semantic/src/expr/test_data/inline_macros @@ -1015,11 +1015,15 @@ foo macro assert_eq { ($x:ident, $y:ident) => { if $x != $y { - panic!("Panic!"); + $defsite::panic(); } }; } +fn panic() { + panic!("Panic!"); +} + //! > expected_diagnostics //! > ========================================================================== @@ -1161,6 +1165,11 @@ macro bas_use_z_from_callsite { fn consume_z(_z: felt252) {} //! > expected_diagnostics +warning: Path in a macro without a resolver modifier ($callsite or $defsite) - currently resolving as $callsite but this will not be supported in future versions. + --> lib.cairo[bas_use_z_from_callsite]:2:9 + z + ^ + error[E0006]: Identifier not found. --> lib.cairo[bas_use_z_from_callsite]:2:9 z @@ -1200,10 +1209,40 @@ macro wrap_bas_use_z_from_callsite { } //! > expected_diagnostics -error: `$callsite` is not supported. - --> lib.cairo[wrap_bas_use_z_from_callsite][bas_use_z_from_callsite]:2:10 +error[E0006]: Identifier not found. + --> lib.cairo[wrap_bas_use_z_from_callsite][bas_use_z_from_callsite]:2:20 $callsite::z - ^^^^^^^^ + ^ + +//! > ========================================================================== + +//! > Test usage of a callsite function with no callsite. + +//! > test_runner_name +test_function_diagnostics(expect_diagnostics: warnings_only) + +//! > function +fn foo() { + call_bar!(); +} + +//! > function_name +foo + +//! > module_code +macro call_bar { + () => { + bar() + }; +} + +fn bar() {} + +//! > expected_diagnostics +warning: Path in a macro without a resolver modifier ($callsite or $defsite) - currently resolving as $callsite but this will not be supported in future versions. + --> lib.cairo[call_bar]:2:9 + bar() + ^^^ //! > ========================================================================== diff --git a/crates/cairo-lang-semantic/src/resolve/mod.rs b/crates/cairo-lang-semantic/src/resolve/mod.rs index fca25fe0fbc..fc3433da3ff 100644 --- a/crates/cairo-lang-semantic/src/resolve/mod.rs +++ b/crates/cairo-lang-semantic/src/resolve/mod.rs @@ -11,6 +11,7 @@ use cairo_lang_defs::ids::{ use cairo_lang_diagnostics::{Maybe, skip_diagnostic}; use cairo_lang_filesystem::db::{CORELIB_CRATE_NAME, CrateSettings}; use cairo_lang_filesystem::ids::{CrateId, CrateLongId}; +use cairo_lang_filesystem::span::TextOffset; use cairo_lang_proc_macros::DebugWithDb; use cairo_lang_syntax as syntax; use cairo_lang_syntax::node::ast::TerminalIdentifier; @@ -50,6 +51,7 @@ use crate::items::generics::generic_params_to_args; use crate::items::imp::{ ConcreteImplId, ConcreteImplLongId, DerefInfo, ImplImplId, ImplLongId, ImplLookupContext, }; +use crate::items::macro_declaration::MacroExpansionResult; use crate::items::module::ModuleItemInfo; use crate::items::trt::{ ConcreteTraitConstantLongId, ConcreteTraitGenericFunctionLongId, ConcreteTraitId, @@ -72,6 +74,11 @@ mod item; // Remove when this becomes an actual crate. const STARKNET_CRATE_NAME: &str = "starknet"; +// Macro related keywords. Notice that the `$` is not included here as it is only a prefix and not a +// part of the segment. +/// The modifier for a macro definition site. +pub const MACRO_CALL_SITE: &str = "callsite"; + /// Lookback maps for item resolving. Can be used to quickly check what is the semantic resolution /// of any path segment. #[derive(Clone, Default, Debug, PartialEq, Eq, DebugWithDb)] @@ -204,12 +211,31 @@ impl ResolverData { } } +/// Resolving data needed for resolving macro expanded code in the correct context. +#[derive(Debug, Clone)] +pub struct ResolverMacroData { + /// The resolver data of the macro definition site. Is used if the path begins with `$defsite`. + pub defsite_data: Arc, + /// The resolver data of the macro call site. Items are resolved in this context in two cases: + /// 1. The path begins with `$callsite`. + /// 2. The path was supplied as a macro argument. In other words, the path is an expansion of a + /// placeholder and is not a part of the macro expansion template. + pub callsite_data: Arc, + /// This is the result of the macro expansion. It is used to determine if a part of the code + /// came from a macro argument or from the macro expansion template. + pub expansion_result: Arc, + /// The parent macro data. Exists in case of a macro calling another macro, and is used if we + /// climp to the callsite environment. + pub parent_macro_call_data: Option>, +} + /// Resolves paths semantically. pub struct Resolver<'db> { db: &'db dyn SemanticGroup, pub data: ResolverData, - /// The resolver data for resolving items from the definition site of a macro. - pub macro_defsite_data: Option>, + /// The resolving context for macro related resolving. Should be `Some` only if the current + /// code is an expansion of a macro. + pub macro_call_data: Option, pub owning_crate_id: CrateId, pub settings: CrateSettings, } @@ -264,6 +290,8 @@ pub trait AsSegments { fn to_segments(self, db: &dyn SyntaxGroup) -> Vec; /// Is the path prefixed with a `$`, indicating a resolver site modifier. fn is_placeholder(&self, db: &dyn SyntaxGroup) -> bool; + /// The offset of the path in the file. + fn offset(&self, db: &dyn SyntaxGroup) -> Option; } impl AsSegments for &ast::ExprPath { fn to_segments(self, db: &dyn SyntaxGroup) -> Vec { @@ -275,6 +303,10 @@ impl AsSegments for &ast::ExprPath { ast::OptionTerminalDollar::TerminalDollar(_) => true, } } + + fn offset(&self, db: &dyn SyntaxGroup) -> Option { + Some(self.as_syntax_node().offset(db)) + } } impl AsSegments for Vec { fn to_segments(self, _: &dyn SyntaxGroup) -> Vec { @@ -285,6 +317,9 @@ impl AsSegments for Vec { // segments. false } + fn offset(&self, db: &dyn SyntaxGroup) -> Option { + self.first().map(|segment| segment.as_syntax_node().offset(db)) + } } impl<'db> Resolver<'db> { @@ -299,7 +334,7 @@ impl<'db> Resolver<'db> { pub fn with_data(db: &'db dyn SemanticGroup, data: ResolverData) -> Self { let owning_crate_id = data.module_file_id.0.owning_crate(db); let settings = db.crate_config(owning_crate_id).map(|c| c.settings).unwrap_or_default(); - Self { owning_crate_id, settings, db, data, macro_defsite_data: None } + Self { owning_crate_id, settings, db, data, macro_call_data: None } } pub fn inference(&mut self) -> Inference<'_> { @@ -349,11 +384,28 @@ impl<'db> Resolver<'db> { let db = self.db; let is_placeholder = path.is_placeholder(db); + let mut cur_offset = path.offset(db).expect("Trying to resolve an empty path."); let elements_vec = path.to_segments(db); let mut segments = elements_vec.iter().peekable(); + let segments_stable_ptr = + segments.peek().expect("Trying to resolve an empty path.").stable_ptr(db); + let mut cur_macro_call_data = self.macro_call_data.clone(); + while let Some(macro_call_data) = cur_macro_call_data.clone() { + if let Some(placeholder_expansion) = + macro_call_data.expansion_result.get_placeholder_at(cur_offset) + { + cur_macro_call_data = macro_call_data.parent_macro_call_data.map(|x| (*x).clone()); + cur_offset = placeholder_expansion.origin.as_span().unwrap().start; + continue; + } + break; + } let active_resolver = if is_placeholder { - &mut self.resolve_placeholder(diagnostics, &mut segments)? + &mut self.resolve_placeholder(diagnostics, &mut segments, cur_macro_call_data)? } else { + if cur_macro_call_data.is_some() { + diagnostics.report(segments_stable_ptr, PathInMacroWithoutModifier); + } self }; // Find where the first segment lies in. @@ -2148,6 +2200,7 @@ impl<'db> Resolver<'db> { &mut self, diagnostics: &mut SemanticDiagnostics, segments: &mut Peekable>, + macro_call_data: Option, ) -> Maybe> { if segments.len() == 1 { return Err(diagnostics.report( @@ -2159,26 +2212,33 @@ impl<'db> Resolver<'db> { Some(ast::PathSegment::Simple(path_segment_simple)) => { let ident = path_segment_simple.ident(self.db); let ident_text = ident.text(self.db); - // TODO(Gil): Add support for $callsite. - if ident_text == MACRO_DEF_SITE { - segments.next(); - if let Some(defsite_resolver_data) = self.macro_defsite_data.as_ref() { - Ok(Resolver::with_data( - self.db, - defsite_resolver_data - .clone_with_inference_id(self.db, self.inference_data.inference_id), - )) - } else { - Err(diagnostics.report( - ident.stable_ptr(self.db), - ResolverModifierNotSupportedInContext, - )) + match ident_text.as_str() { + MACRO_DEF_SITE | MACRO_CALL_SITE => { + segments.next(); + if let Some(macro_call_data) = macro_call_data { + let resolver_data = if ident_text.as_str() == MACRO_DEF_SITE { + macro_call_data.defsite_data + } else { + macro_call_data.callsite_data + }; + Ok(Resolver::with_data( + self.db, + resolver_data.clone_with_inference_id( + self.db, + self.inference_data.inference_id, + ), + )) + } else { + Err(diagnostics.report( + ident.stable_ptr(self.db), + ResolverModifierNotSupportedInContext, + )) + } } - } else { - Err(diagnostics.report( + _ => Err(diagnostics.report( ident.stable_ptr(self.db), UnknownResolverModifier { modifier: ident_text }, - )) + )), } } _ => {