Skip to main content

triblespace_macros/
lib.rs

1use proc_macro::Span;
2use proc_macro::TokenStream;
3
4use proc_macro2::TokenStream as TokenStream2;
5use quote::{quote, ToTokens};
6
7use std::path::Path;
8
9use ed25519_dalek::SigningKey;
10use hex::FromHex;
11
12use triblespace_core::id::fucid;
13use triblespace_core::id::Id;
14use triblespace_core::repo::pile::Pile;
15use triblespace_core::repo::Repository;
16use triblespace_core::repo::Workspace;
17use triblespace_core::trible::TribleSet;
18use triblespace_core::value::schemas::hash::Blake3;
19
20use syn::parse::Parse;
21use syn::parse::ParseStream;
22use syn::Attribute;
23use syn::Ident;
24use syn::LitStr;
25use syn::Token;
26use syn::Type;
27use syn::Visibility;
28
29use triblespace_macros_common::{
30    attributes_impl, entity_impl, path_impl, pattern_changes_impl, pattern_impl,
31    value_formatter_impl,
32};
33
34mod instrumentation_attributes {
35    /// Attributes specific to compile-time attribute definition instrumentation.
36    /// Reuses `metadata::name`, `metadata::attribute`, and `metadata::tag` for
37    /// fields that match their runtime `describe()` counterparts.
38    pub(crate) mod attribute {
39        use triblespace_core::blob::schemas::longstring::LongString;
40        use triblespace_core::prelude::valueschemas::{Blake3, Handle, ShortString};
41        use triblespace_core_macros::attributes;
42
43        attributes! {
44            // Instrumentation-specific: link back to the macro invocation entity.
45            "19D4972B2DF977FA64541FC967C4B133" as invocation: ShortString;
46            // Instrumentation-specific: the Rust type tokens for this attribute's value schema.
47            "D97A427FF782B0BF08B55AC84877B486" as attribute_type: Handle<Blake3, LongString>;
48        }
49    }
50
51    pub(crate) mod invocation {
52        use triblespace_core::blob::schemas::longstring::LongString;
53        use triblespace_core::prelude::valueschemas::{Blake3, Handle, LineLocation, ShortString};
54        use triblespace_core_macros::attributes;
55
56        attributes! {
57            "1CED5213A71C9DD60AD9B3698E5548F4" as macro_kind: ShortString;
58            "E413CB09A4352D7B46B65FC635C18CCC" as manifest_dir: Handle<Blake3, LongString>;
59            "8ED33DA54C226ADEA0FFF7863563DF5F" as source_range: LineLocation;
60            "B981AEA9437561F8DB96E7EECBB94BFD" as source_tokens: Handle<Blake3, LongString>;
61            "92EF719DA3DD2405E89B953837E076A5" as crate_name: ShortString;
62        }
63    }
64}
65
66use instrumentation_attributes::attribute;
67use instrumentation_attributes::invocation;
68
69fn invocation_span(input: &TokenStream) -> Span {
70    let mut iter = input.clone().into_iter();
71    iter.next()
72        .map(|tt| tt.span())
73        .unwrap_or_else(Span::call_site)
74}
75
76fn parse_signing_key(value: &str) -> Option<[u8; 32]> {
77    <[u8; 32]>::from_hex(value).ok()
78}
79
80fn metadata_signing_key() -> Option<SigningKey> {
81    let value = std::env::var("TRIBLESPACE_METADATA_SIGNING_KEY").ok()?;
82    let bytes = parse_signing_key(&value)?;
83    Some(SigningKey::from_bytes(&bytes))
84}
85
86fn parse_branch_id(value: &str) -> Option<Id> {
87    Id::from_hex(value)
88}
89
90struct MetadataContext<'a> {
91    workspace: &'a mut Workspace<Pile<Blake3>>,
92    invocation_id: triblespace_core::id::Id,
93    input: &'a TokenStream,
94}
95
96impl<'a> MetadataContext<'a> {
97    fn workspace(&mut self) -> &mut Workspace<Pile<Blake3>> {
98        self.workspace
99    }
100
101    fn invocation_id(&self) -> triblespace_core::id::Id {
102        self.invocation_id
103    }
104
105    fn tokens(&self) -> &'a TokenStream {
106        self.input
107    }
108}
109
110fn emit_metadata<F>(kind: &str, input: &TokenStream, extra: F)
111where
112    F: FnOnce(&mut MetadataContext<'_>),
113{
114    let pile_path = match std::env::var("TRIBLESPACE_METADATA_PILE") {
115        Ok(p) if !p.trim().is_empty() => p,
116        _ => return,
117    };
118
119    let branch_value = match std::env::var("TRIBLESPACE_METADATA_BRANCH") {
120        Ok(b) if !b.trim().is_empty() => b,
121        _ => return,
122    };
123
124    let branch_id = match parse_branch_id(&branch_value) {
125        Some(id) => id,
126        None => return,
127    };
128
129    let pile = match Pile::<Blake3>::open(Path::new(&pile_path)) {
130        Ok(pile) => pile,
131        Err(_) => return,
132    };
133
134    let signing_key = match metadata_signing_key() {
135        Some(key) => key,
136        None => {
137            // Avoid Drop warnings if metadata emission is partially configured.
138            let _ = pile.close();
139            return;
140        }
141    };
142    let mut repo = match Repository::new(pile, signing_key, TribleSet::new()) {
143        Ok(r) => r,
144        Err(_) => return,
145    };
146
147    let mut workspace = match repo.pull(branch_id) {
148        Ok(ws) => ws,
149        Err(_) => {
150            let _ = repo.close();
151            return;
152        }
153    };
154
155    let span = invocation_span(input);
156    let mut set = TribleSet::new();
157    let entity = fucid();
158    let invocation_id = entity.id;
159
160    set += ::triblespace_core::macros::entity! {
161        &entity @
162        invocation::macro_kind: kind,
163        invocation::source_range: span
164    };
165
166    if let Ok(crate_name) = std::env::var("CARGO_PKG_NAME") {
167        set += ::triblespace_core::macros::entity! { &entity @ invocation::crate_name: crate_name };
168    }
169
170    if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
171        if !dir.trim().is_empty() {
172            let handle = workspace.put(dir);
173            set +=
174                ::triblespace_core::macros::entity! { &entity @ invocation::manifest_dir: handle };
175        }
176    }
177
178    let tokens = input.to_string();
179    if !tokens.is_empty() {
180        let handle = workspace.put(tokens);
181        set += ::triblespace_core::macros::entity! { &entity @ invocation::source_tokens: handle };
182    }
183
184    if set.is_empty() {
185        let _ = repo.close();
186        return;
187    }
188
189    workspace.commit(set, "macro invocation");
190
191    {
192        let mut context = MetadataContext {
193            workspace: &mut workspace,
194            invocation_id,
195            input,
196        };
197        extra(&mut context);
198    }
199
200    let _ = repo.push(&mut workspace);
201
202    drop(workspace);
203    let _ = repo.close();
204}
205
206struct AttributeDefinition {
207    id: LitStr,
208    name: Ident,
209    ty: Type,
210}
211
212struct AttributeDefinitions {
213    entries: Vec<AttributeDefinition>,
214}
215
216impl Parse for AttributeDefinitions {
217    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
218        let mut entries = Vec::new();
219        while !input.is_empty() {
220            let _ = input.call(Attribute::parse_outer)?;
221            if input.peek(Token![pub]) {
222                let v: Visibility = input.parse()?;
223                return Err(syn::Error::new_spanned(
224                    v,
225                    "visibility must appear after `as` and before the attribute name (e.g. `\"...\" as pub name: Type;`)",
226                ));
227            }
228
229            let id: LitStr = input.parse()?;
230            input.parse::<Token![as]>()?;
231            if input.peek(Token![pub]) {
232                let _: Visibility = input.parse()?;
233            }
234            let name: Ident = input.parse()?;
235            input.parse::<Token![:]>()?;
236            let ty: Type = input.parse()?;
237            input.parse::<Token![;]>()?;
238
239            entries.push(AttributeDefinition { id, name, ty });
240        }
241        Ok(AttributeDefinitions { entries })
242    }
243}
244
245fn emit_attribute_definitions(context: &mut MetadataContext<'_>) {
246    use triblespace_core::metadata;
247    use triblespace_core::prelude::ValueSchema;
248    use triblespace_core::value::schemas::genid::GenId;
249
250    let Ok(parsed) =
251        syn::parse2::<AttributeDefinitions>(TokenStream2::from(context.tokens().clone()))
252    else {
253        return;
254    };
255    if parsed.entries.is_empty() {
256        return;
257    }
258
259    let invocation_hex = format!("{:X}", context.invocation_id());
260
261    for definition in parsed.entries {
262        let entity = fucid();
263
264        // Parse the attribute hex ID into a proper Id for GenId storage.
265        let Some(attr_id) = Id::from_hex(&definition.id.value()) else {
266            continue;
267        };
268
269        let name_handle = context.workspace().put(definition.name.to_string());
270        let mut set = ::triblespace_core::macros::entity! {
271            &entity @
272            metadata::attribute: GenId::value_from(attr_id),
273            metadata::name: name_handle,
274            metadata::tag: metadata::KIND_ATTRIBUTE_USAGE,
275            attribute::invocation: invocation_hex.as_str()
276        };
277
278        let ty_tokens = definition.ty.to_token_stream().to_string();
279        if !ty_tokens.is_empty() {
280            let handle = context.workspace().put(ty_tokens);
281            set +=
282                ::triblespace_core::macros::entity! { &entity @ attribute::attribute_type: handle };
283        }
284
285        context.workspace().commit(set, "macro invocation");
286    }
287}
288
289#[proc_macro]
290pub fn attributes(input: TokenStream) -> TokenStream {
291    let clone = input.clone();
292    emit_metadata("attributes", &clone, |context| {
293        emit_attribute_definitions(context)
294    });
295    let base_path: TokenStream2 = quote!(::triblespace::core);
296    let tokens = TokenStream2::from(input);
297    match attributes_impl(tokens, &base_path) {
298        Ok(ts) => TokenStream::from(ts),
299        Err(e) => e.to_compile_error().into(),
300    }
301}
302
303#[proc_macro]
304pub fn path(input: TokenStream) -> TokenStream {
305    let clone = input.clone();
306    emit_metadata("path", &clone, |_context| {});
307    let base_path: TokenStream2 = quote!(::triblespace::core);
308    let tokens = TokenStream2::from(input);
309    match path_impl(tokens, &base_path) {
310        Ok(ts) => TokenStream::from(ts),
311        Err(e) => e.to_compile_error().into(),
312    }
313}
314
315#[proc_macro]
316pub fn pattern(input: TokenStream) -> TokenStream {
317    let clone = input.clone();
318    emit_metadata("pattern", &clone, |_context| {});
319    let base_path: TokenStream2 = quote!(::triblespace::core);
320    let tokens = TokenStream2::from(input);
321    match pattern_impl(tokens, &base_path) {
322        Ok(ts) => TokenStream::from(ts),
323        Err(e) => e.to_compile_error().into(),
324    }
325}
326
327#[proc_macro]
328pub fn pattern_changes(input: TokenStream) -> TokenStream {
329    let clone = input.clone();
330    emit_metadata("pattern_changes", &clone, |_context| {});
331    let base_path: TokenStream2 = quote!(::triblespace::core);
332    let tokens = TokenStream2::from(input);
333    match pattern_changes_impl(tokens, &base_path) {
334        Ok(ts) => TokenStream::from(ts),
335        Err(e) => e.to_compile_error().into(),
336    }
337}
338
339#[proc_macro]
340pub fn entity(input: TokenStream) -> TokenStream {
341    let clone = input.clone();
342    emit_metadata("entity", &clone, |_context| {});
343    let base_path: TokenStream2 = quote!(::triblespace::core);
344    let tokens = TokenStream2::from(input);
345    match entity_impl(tokens, &base_path) {
346        Ok(ts) => TokenStream::from(ts),
347        Err(e) => e.to_compile_error().into(),
348    }
349}
350
351#[proc_macro]
352pub fn find(input: TokenStream) -> TokenStream {
353    let clone = input.clone();
354    emit_metadata("find", &clone, |_context| {});
355    let inner = TokenStream2::from(input);
356    TokenStream::from(quote!(::triblespace::core::macros::find!(#inner)))
357}
358
359#[proc_macro]
360pub fn exists(input: TokenStream) -> TokenStream {
361    let clone = input.clone();
362    emit_metadata("exists", &clone, |_context| {});
363    let inner = TokenStream2::from(input);
364    TokenStream::from(quote!(::triblespace::core::exists!(#inner)))
365}
366
367#[proc_macro_attribute]
368pub fn value_formatter(attr: TokenStream, item: TokenStream) -> TokenStream {
369    let clone = item.clone();
370    emit_metadata("value_formatter", &clone, |_context| {});
371
372    match value_formatter_impl(TokenStream2::from(attr), TokenStream2::from(item)) {
373        Ok(tokens) => TokenStream::from(tokens),
374        Err(err) => err.to_compile_error().into(),
375    }
376}