diff --git a/Cargo.lock b/Cargo.lock index 7e39679b2..b2a525353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,7 +741,6 @@ dependencies = [ name = "cairo-lang-macro-attributes" version = "0.0.1" dependencies = [ - "camino", "quote", "syn 2.0.52", ] diff --git a/plugins/cairo-lang-macro-attributes/Cargo.toml b/plugins/cairo-lang-macro-attributes/Cargo.toml index 1d3787117..cd5b4a640 100644 --- a/plugins/cairo-lang-macro-attributes/Cargo.toml +++ b/plugins/cairo-lang-macro-attributes/Cargo.toml @@ -16,6 +16,5 @@ repository.workspace = true proc-macro = true [dependencies] -camino.workspace = true quote.workspace = true syn = { workspace = true, features = ["full", "extra-traits"] } diff --git a/plugins/cairo-lang-macro-stable/src/ffi.rs b/plugins/cairo-lang-macro-stable/src/ffi.rs new file mode 100644 index 000000000..c0701f297 --- /dev/null +++ b/plugins/cairo-lang-macro-stable/src/ffi.rs @@ -0,0 +1,44 @@ +/// This struct encapsulates a stable ABI representation of struct. +/// +/// Please be aware that the memory management of values passing the FFI-barrier is tricky. +#[repr(C)] +#[derive(Debug)] +#[doc(hidden)] +pub struct StableSlice { + ptr: *mut T, + len: usize, +} + +impl Copy for StableSlice {} + +impl Clone for StableSlice { + fn clone(&self) -> Self { + *self + } +} + +impl StableSlice { + /// Create a new `StableSlice` from a Vector. + /// Note that the vector will not be deallocated automatically. + /// Please make sure to use `into_owned` afterward, to free the memory. + pub fn new(mut x: Vec) -> Self { + x.shrink_to_fit(); + assert_eq!(x.len(), x.capacity()); + let ptr = x.as_mut_ptr(); + let len = x.len(); + std::mem::forget(x); + Self { ptr, len } + } + + /// Convert to owned vector. + pub fn into_owned(self) -> Vec { + unsafe { Vec::from_raw_parts(self.ptr, self.len, self.len) } + } + + /// Returns raw pointer and length. + /// Can be used to construct a slice. + /// No ownership is transferred. + pub fn into_raw_parts(self) -> (*mut T, usize) { + (self.ptr, self.len) + } +} diff --git a/plugins/cairo-lang-macro-stable/src/lib.rs b/plugins/cairo-lang-macro-stable/src/lib.rs index a431094a0..5e33a5398 100644 --- a/plugins/cairo-lang-macro-stable/src/lib.rs +++ b/plugins/cairo-lang-macro-stable/src/lib.rs @@ -1,6 +1,9 @@ +use crate::ffi::StableSlice; use std::ffi::{CStr, CString}; use std::os::raw::c_char; +pub mod ffi; + /// Token stream. /// /// This struct implements FFI-safe stable ABI. @@ -15,6 +18,26 @@ pub enum StableAuxData { Some(*mut c_char), } +/// Diagnostic returned by the procedural macro. +/// +/// This struct implements FFI-safe stable ABI. +#[repr(C)] +#[derive(Debug)] +pub struct StableDiagnostic { + pub message: *mut c_char, + pub severity: StableSeverity, +} + +/// The severity of a diagnostic. +/// +/// This struct implements FFI-safe stable ABI. +#[repr(C)] +#[derive(Debug)] +pub enum StableSeverity { + Error, + Warning, +} + /// Procedural macro result. /// /// This struct implements FFI-safe stable ABI. @@ -22,14 +45,25 @@ pub enum StableAuxData { #[derive(Debug, Clone)] pub enum StableProcMacroResult { /// Plugin has not taken any action. - Leave, + Leave { + diagnostics: StableSlice, + // diagnostics: *mut StableDiagnostic, + // diagnostics_n: usize, + }, /// Plugin generated [`StableTokenStream`] replacement. Replace { + diagnostics: StableSlice, token_stream: StableTokenStream, aux_data: StableAuxData, + // diagnostics: *mut StableDiagnostic, + // diagnostics_n: usize, }, /// Plugin ordered item removal. - Remove, + Remove { + diagnostics: StableSlice, + // diagnostics: *mut StableDiagnostic, + // diagnostics_n: usize, + }, } impl StableTokenStream { diff --git a/plugins/cairo-lang-macro/src/lib.rs b/plugins/cairo-lang-macro/src/lib.rs index 735f37504..e50a68db3 100644 --- a/plugins/cairo-lang-macro/src/lib.rs +++ b/plugins/cairo-lang-macro/src/lib.rs @@ -1,20 +1,24 @@ +pub use cairo_lang_macro_attributes::*; +use cairo_lang_macro_stable::ffi::StableSlice; +use cairo_lang_macro_stable::{ + StableAuxData, StableDiagnostic, StableProcMacroResult, StableSeverity, StableTokenStream, +}; use std::ffi::{c_char, CStr, CString}; use std::fmt::Display; - -pub use cairo_lang_macro_attributes::*; -use cairo_lang_macro_stable::{StableAuxData, StableProcMacroResult, StableTokenStream}; +use std::slice; #[derive(Debug)] pub enum ProcMacroResult { /// Plugin has not taken any action. - Leave, + Leave { diagnostics: Vec }, /// Plugin generated [`TokenStream`] replacement. Replace { token_stream: TokenStream, aux_data: Option, + diagnostics: Vec, }, /// Plugin ordered item removal. - Remove, + Remove { diagnostics: Vec }, } #[derive(Debug, Default, Clone)] @@ -53,6 +57,36 @@ impl Display for AuxData { } } +/// Diagnostic returned by the procedural macro. +#[derive(Debug)] +pub struct Diagnostic { + pub message: String, + pub severity: Severity, +} + +/// The severity of a diagnostic. +#[derive(Debug)] +pub enum Severity { + Error, + Warning, +} + +impl Diagnostic { + pub fn error(message: impl ToString) -> Self { + Self { + message: message.to_string(), + severity: Severity::Error, + } + } + + pub fn warn(message: impl ToString) -> Self { + Self { + message: message.to_string(), + severity: Severity::Warning, + } + } +} + impl ProcMacroResult { /// Convert to FFI-safe representation. /// @@ -60,15 +94,39 @@ impl ProcMacroResult { #[doc(hidden)] pub fn into_stable(self) -> StableProcMacroResult { match self { - ProcMacroResult::Leave => StableProcMacroResult::Leave, - ProcMacroResult::Remove => StableProcMacroResult::Remove, + ProcMacroResult::Leave { diagnostics } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Leave { + diagnostics: StableSlice::new(diagnostics), + } + } + ProcMacroResult::Remove { diagnostics } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Remove { + diagnostics: StableSlice::new(diagnostics), + } + } ProcMacroResult::Replace { token_stream, aux_data, - } => StableProcMacroResult::Replace { - token_stream: token_stream.into_stable(), - aux_data: AuxData::maybe_into_stable(aux_data), - }, + diagnostics, + } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Replace { + token_stream: token_stream.into_stable(), + aux_data: AuxData::maybe_into_stable(aux_data), + diagnostics: StableSlice::new(diagnostics), + } + } } } @@ -80,15 +138,38 @@ impl ProcMacroResult { #[doc(hidden)] pub unsafe fn from_stable(result: StableProcMacroResult) -> Self { match result { - StableProcMacroResult::Leave => ProcMacroResult::Leave, - StableProcMacroResult::Remove => ProcMacroResult::Remove, + StableProcMacroResult::Leave { diagnostics } => { + let (ptr, n) = diagnostics.into_raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Leave { diagnostics } + } + StableProcMacroResult::Remove { diagnostics } => { + let (ptr, n) = diagnostics.into_raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Remove { diagnostics } + } StableProcMacroResult::Replace { token_stream, aux_data, - } => ProcMacroResult::Replace { - token_stream: TokenStream::from_stable(token_stream), - aux_data: AuxData::from_stable(aux_data), - }, + diagnostics, + } => { + let (ptr, n) = diagnostics.into_raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Replace { + token_stream: TokenStream::from_stable(token_stream), + aux_data: AuxData::from_stable(aux_data), + diagnostics, + } + } } } @@ -101,15 +182,38 @@ impl ProcMacroResult { #[doc(hidden)] pub unsafe fn from_owned_stable(result: StableProcMacroResult) -> Self { match result { - StableProcMacroResult::Leave => ProcMacroResult::Leave, - StableProcMacroResult::Remove => ProcMacroResult::Remove, + StableProcMacroResult::Leave { diagnostics } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Leave { diagnostics } + } + StableProcMacroResult::Remove { diagnostics } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Remove { diagnostics } + } StableProcMacroResult::Replace { token_stream, aux_data, - } => ProcMacroResult::Replace { - token_stream: TokenStream::from_owned_stable(token_stream), - aux_data: AuxData::from_owned_stable(aux_data), - }, + diagnostics, + } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Replace { + token_stream: TokenStream::from_owned_stable(token_stream), + aux_data: AuxData::from_owned_stable(aux_data), + diagnostics, + } + } } } } @@ -195,6 +299,70 @@ impl AuxData { } } +impl Diagnostic { + // Convert to FFI-safe representation. + /// + /// # Safety + #[doc(hidden)] + pub fn into_stable(self) -> StableDiagnostic { + StableDiagnostic { + message: CString::new(self.message).unwrap().into_raw(), + severity: self.severity.into_stable(), + } + } + + /// Convert to native Rust representation, without taking the ownership of the string. + /// + /// Note that you still need to free the memory by calling `from_owned_stable`. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(diagnostic: &StableDiagnostic) -> Self { + Self { + message: from_raw_cstr(diagnostic.message), + severity: Severity::from_stable(&diagnostic.severity), + } + } + + /// Convert to native Rust representation, with taking the ownership of the string. + /// + /// Useful when you need to free the allocated memory. + /// Only use on the same side of FFI-barrier, where the memory has been allocated. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_owned_stable(diagnostic: StableDiagnostic) -> Self { + Self { + message: from_raw_cstring(diagnostic.message), + severity: Severity::from_stable(&diagnostic.severity), + } + } +} + +impl Severity { + /// Convert to FFI-safe representation. + /// # Safety + /// + #[doc(hidden)] + pub fn into_stable(self) -> StableSeverity { + match self { + Severity::Error => StableSeverity::Error, + Severity::Warning => StableSeverity::Warning, + } + } + + /// Convert to native Rust representation. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(severity: &StableSeverity) -> Self { + match severity { + StableSeverity::Error => Self::Error, + StableSeverity::Warning => Self::Warning, + } + } +} + // Create a string from a raw pointer to a c_char. // Note that this will free the underlying memory. unsafe fn from_raw_cstring(raw: *mut c_char) -> String { diff --git a/scarb/src/compiler/plugin/proc_macro/host.rs b/scarb/src/compiler/plugin/proc_macro/host.rs index 34fdc719d..4d3c36e9b 100644 --- a/scarb/src/compiler/plugin/proc_macro/host.rs +++ b/scarb/src/compiler/plugin/proc_macro/host.rs @@ -1,15 +1,17 @@ use crate::compiler::plugin::proc_macro::{FromItemAst, ProcMacroInstance}; use crate::core::{Config, Package, PackageId}; use anyhow::Result; +use cairo_lang_defs::plugin::PluginDiagnostic; use cairo_lang_defs::plugin::{ DynGeneratedFileAuxData, GeneratedFileAuxData, MacroPlugin, MacroPluginMetadata, PluginGeneratedFile, PluginResult, }; -use cairo_lang_macro::{AuxData, ProcMacroResult, TokenStream}; +use cairo_lang_macro::{AuxData, Diagnostic, ProcMacroResult, Severity, TokenStream}; use cairo_lang_semantic::plugin::PluginSuite; use cairo_lang_syntax::attribute::structured::AttributeListStructurize; -use cairo_lang_syntax::node::ast; use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::ids::SyntaxStablePtrId; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; use itertools::Itertools; use smol_str::SmolStr; use std::any::Any; @@ -141,10 +143,12 @@ impl MacroPlugin for ProcMacroHostPlugin { .into_iter() .chain(self.handle_attribute(db, item_ast.clone())) .chain(self.handle_derive(db, item_ast.clone())); + let stable_ptr = item_ast.clone().stable_ptr().untyped(); let mut token_stream = TokenStream::from_item_ast(db, item_ast); let mut aux_data: Option = None; let mut modified = false; + let mut all_diagnostics: Vec = Vec::new(); for input in expansions { let instance = self .macros @@ -155,19 +159,24 @@ impl MacroPlugin for ProcMacroHostPlugin { ProcMacroResult::Replace { token_stream: new_token_stream, aux_data: new_aux_data, + diagnostics, } => { token_stream = new_token_stream; aux_data = new_aux_data; modified = true; + all_diagnostics.extend(diagnostics); } - ProcMacroResult::Remove => { + ProcMacroResult::Remove { diagnostics } => { + all_diagnostics.extend(diagnostics); return PluginResult { + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), code: None, - diagnostics: Vec::new(), remove_original_item: true, - } + }; + } + ProcMacroResult::Leave { diagnostics } => { + all_diagnostics.extend(diagnostics); } - ProcMacroResult::Leave => {} }; } if modified { @@ -179,11 +188,15 @@ impl MacroPlugin for ProcMacroHostPlugin { aux_data: aux_data .map(|ad| DynGeneratedFileAuxData::new(ProcMacroAuxData(ad.to_string()))), }), - diagnostics: Vec::new(), + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), remove_original_item: true, } } else { - PluginResult::default() + PluginResult { + code: None, + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), + remove_original_item: false, + } } } @@ -195,6 +208,23 @@ impl MacroPlugin for ProcMacroHostPlugin { } } +fn into_cairo_diagnostics( + diagnostics: Vec, + stable_ptr: SyntaxStablePtrId, +) -> Vec { + diagnostics + .into_iter() + .map(|diag| PluginDiagnostic { + stable_ptr, + message: diag.message, + severity: match diag.severity { + Severity::Error => cairo_lang_diagnostics::Severity::Error, + Severity::Warning => cairo_lang_diagnostics::Severity::Warning, + }, + }) + .collect_vec() +} + /// A Scarb wrapper around the `ProcMacroHost` compiler plugin. /// /// This struct represent the compiler plugin in terms of Scarb data model. diff --git a/scarb/tests/build_cairo_plugin.rs b/scarb/tests/build_cairo_plugin.rs index ae08c3d6e..4882744cf 100644 --- a/scarb/tests/build_cairo_plugin.rs +++ b/scarb/tests/build_cairo_plugin.rs @@ -59,31 +59,21 @@ fn lib_path(lib_name: &str) -> String { serde_json::to_string(&path).unwrap() } -fn simple_project(t: &impl PathChild) { +fn simple_project_with_code(t: &impl PathChild, code: impl ToString) { let macro_lib_path = lib_path("cairo-lang-macro"); let macro_stable_lib_path = lib_path("cairo-lang-macro-stable"); CairoPluginProjectBuilder::start() .scarb_project(|b| { - b.name("hello") + b.name("some") .version("1.0.0") .manifest_extra(r#"[cairo-plugin]"#) }) - .lib_rs(indoc! {r#" - use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons}; - - macro_commons!(); - - #[attribute_macro] - pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { - let _code = token_stream.to_string(); - ProcMacroResult::Leave - } - "#}) + .lib_rs(code) .src( "Cargo.toml", formatdoc! {r#" [package] - name = "proc-macro-stub" + name = "some" version = "0.1.0" edition = "2021" publish = false @@ -99,6 +89,21 @@ fn simple_project(t: &impl PathChild) { .build(t); } +fn simple_project(t: &impl PathChild) { + let code = indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + ProcMacroResult::Leave { diagnostics: Vec::new() } + } + "#}; + simple_project_with_code(t, code); +} + #[test] fn compile_cairo_plugin() { let t = TempDir::new().unwrap(); @@ -117,7 +122,7 @@ fn compile_cairo_plugin() { String::from_utf8_lossy(&output.stderr), ); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(stdout.contains("Compiling hello v1.0.0")); + assert!(stdout.contains("Compiling some v1.0.0")); let lines = stdout.lines().map(ToString::to_string).collect::>(); let (last, lines) = lines.split_last().unwrap(); assert_matches(r#"[..] Finished release target(s) in [..]"#, last); @@ -143,7 +148,7 @@ fn check_cairo_plugin() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(stdout.contains("Checking hello v1.0.0")); + assert!(stdout.contains("Checking some v1.0.0")); let lines = stdout.lines().map(ToString::to_string).collect::>(); let (last, lines) = lines.split_last().unwrap(); assert_matches(r#"[..] Finished checking release target(s) in [..]"#, last); @@ -193,7 +198,7 @@ fn can_use_json_output() { let lines = stdout.lines().map(ToString::to_string).collect::>(); let (first, lines) = lines.split_first().unwrap(); assert_matches( - r#"{"status":"checking","message":"hello v1.0.0 ([..]Scarb.toml)"}"#, + r#"{"status":"checking","message":"some v1.0.0 ([..]Scarb.toml)"}"#, first, ); let (last, lines) = lines.split_last().unwrap(); @@ -255,3 +260,101 @@ fn compile_cairo_plugin_with_other_target() { target `cairo-plugin` cannot be mixed with other targets "#}); } + +#[test] +fn can_emit_plugin_warning() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + simple_project_with_code( + &t, + indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons, Diagnostic}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + let diag = Diagnostic::warn("Some warning from macro."); + ProcMacroResult::Leave { diagnostics: vec![diag] } + } + "#}, + ); + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn f() -> felt252 { 12 } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&project) + .assert() + .success() + .stdout_matches(indoc! {r#" + [..] Compiling some v1.0.0 ([..]Scarb.toml) + [..] Compiling hello v1.0.0 ([..]Scarb.toml) + warn: Plugin diagnostic: Some warning from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + [..]Finished release target(s) in [..] + "#}); +} + +#[test] +fn can_emit_plugin_error() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + simple_project_with_code( + &t, + indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons, Diagnostic}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + let diag = Diagnostic::error("Some error from macro."); + ProcMacroResult::Leave { diagnostics: vec![diag] } + } + "#}, + ); + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn f() -> felt252 { 12 } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&project) + .assert() + .failure() + .stdout_matches(indoc! {r#" + [..] Compiling some v1.0.0 ([..]Scarb.toml) + [..] Compiling hello v1.0.0 ([..]Scarb.toml) + error: Plugin diagnostic: Some error from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + error: could not compile `hello` due to previous error + "#}); +}