triblespace-macros 0.32.0

Instrumented wrappers around the triblespace procedural macros
Documentation
use proc_macro::Span;
use proc_macro::TokenStream;

use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};

use std::path::Path;

use ed25519_dalek::SigningKey;
use hex::FromHex;

use triblespace_core::id::fucid;
use triblespace_core::id::Id;
use triblespace_core::repo::pile::Pile;
use triblespace_core::repo::Repository;
use triblespace_core::repo::Workspace;
use triblespace_core::trible::TribleSet;
use triblespace_core::value::schemas::hash::Blake3;

use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::Attribute;
use syn::Ident;
use syn::LitStr;
use syn::Token;
use syn::Type;
use syn::Visibility;

use triblespace_macros_common::{
    attributes_impl, entity_impl, path_impl, pattern_changes_impl, pattern_impl,
    value_formatter_impl,
};

mod instrumentation_attributes {
    /// Attributes specific to compile-time attribute definition instrumentation.
    /// Reuses `metadata::name`, `metadata::attribute`, and `metadata::tag` for
    /// fields that match their runtime `describe()` counterparts.
    pub(crate) mod attribute {
        use triblespace_core::blob::schemas::longstring::LongString;
        use triblespace_core::prelude::valueschemas::{Blake3, Handle, ShortString};
        use triblespace_core_macros::attributes;

        attributes! {
            // Instrumentation-specific: link back to the macro invocation entity.
            "19D4972B2DF977FA64541FC967C4B133" as invocation: ShortString;
            // Instrumentation-specific: the Rust type tokens for this attribute's value schema.
            "D97A427FF782B0BF08B55AC84877B486" as attribute_type: Handle<Blake3, LongString>;
        }
    }

    pub(crate) mod invocation {
        use triblespace_core::blob::schemas::longstring::LongString;
        use triblespace_core::prelude::valueschemas::{Blake3, Handle, LineLocation, ShortString};
        use triblespace_core_macros::attributes;

        attributes! {
            "1CED5213A71C9DD60AD9B3698E5548F4" as macro_kind: ShortString;
            "E413CB09A4352D7B46B65FC635C18CCC" as manifest_dir: Handle<Blake3, LongString>;
            "8ED33DA54C226ADEA0FFF7863563DF5F" as source_range: LineLocation;
            "B981AEA9437561F8DB96E7EECBB94BFD" as source_tokens: Handle<Blake3, LongString>;
            "92EF719DA3DD2405E89B953837E076A5" as crate_name: ShortString;
        }
    }
}

use instrumentation_attributes::attribute;
use instrumentation_attributes::invocation;

fn invocation_span(input: &TokenStream) -> Span {
    let mut iter = input.clone().into_iter();
    iter.next()
        .map(|tt| tt.span())
        .unwrap_or_else(Span::call_site)
}

fn parse_signing_key(value: &str) -> Option<[u8; 32]> {
    <[u8; 32]>::from_hex(value).ok()
}

fn metadata_signing_key() -> Option<SigningKey> {
    let value = std::env::var("TRIBLESPACE_METADATA_SIGNING_KEY").ok()?;
    let bytes = parse_signing_key(&value)?;
    Some(SigningKey::from_bytes(&bytes))
}

fn parse_branch_id(value: &str) -> Option<Id> {
    Id::from_hex(value)
}

struct MetadataContext<'a> {
    workspace: &'a mut Workspace<Pile<Blake3>>,
    invocation_id: triblespace_core::id::Id,
    input: &'a TokenStream,
}

impl<'a> MetadataContext<'a> {
    fn workspace(&mut self) -> &mut Workspace<Pile<Blake3>> {
        self.workspace
    }

    fn invocation_id(&self) -> triblespace_core::id::Id {
        self.invocation_id
    }

    fn tokens(&self) -> &'a TokenStream {
        self.input
    }
}

fn emit_metadata<F>(kind: &str, input: &TokenStream, extra: F)
where
    F: FnOnce(&mut MetadataContext<'_>),
{
    let pile_path = match std::env::var("TRIBLESPACE_METADATA_PILE") {
        Ok(p) if !p.trim().is_empty() => p,
        _ => return,
    };

    let branch_value = match std::env::var("TRIBLESPACE_METADATA_BRANCH") {
        Ok(b) if !b.trim().is_empty() => b,
        _ => return,
    };

    let branch_id = match parse_branch_id(&branch_value) {
        Some(id) => id,
        None => return,
    };

    let pile = match Pile::<Blake3>::open(Path::new(&pile_path)) {
        Ok(pile) => pile,
        Err(_) => return,
    };

    let signing_key = match metadata_signing_key() {
        Some(key) => key,
        None => {
            // Avoid Drop warnings if metadata emission is partially configured.
            let _ = pile.close();
            return;
        }
    };
    let mut repo = match Repository::new(pile, signing_key, TribleSet::new()) {
        Ok(r) => r,
        Err(_) => return,
    };

    let mut workspace = match repo.pull(branch_id) {
        Ok(ws) => ws,
        Err(_) => {
            let _ = repo.close();
            return;
        }
    };

    let span = invocation_span(input);
    let mut set = TribleSet::new();
    let entity = fucid();
    let invocation_id = entity.id;

    set += ::triblespace_core::macros::entity! {
        &entity @
        invocation::macro_kind: kind,
        invocation::source_range: span
    };

    if let Ok(crate_name) = std::env::var("CARGO_PKG_NAME") {
        set += ::triblespace_core::macros::entity! { &entity @ invocation::crate_name: crate_name };
    }

    if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
        if !dir.trim().is_empty() {
            let handle = workspace.put(dir);
            set +=
                ::triblespace_core::macros::entity! { &entity @ invocation::manifest_dir: handle };
        }
    }

    let tokens = input.to_string();
    if !tokens.is_empty() {
        let handle = workspace.put(tokens);
        set += ::triblespace_core::macros::entity! { &entity @ invocation::source_tokens: handle };
    }

    if set.is_empty() {
        let _ = repo.close();
        return;
    }

    workspace.commit(set, "macro invocation");

    {
        let mut context = MetadataContext {
            workspace: &mut workspace,
            invocation_id,
            input,
        };
        extra(&mut context);
    }

    let _ = repo.push(&mut workspace);

    drop(workspace);
    let _ = repo.close();
}

struct AttributeDefinition {
    id: LitStr,
    name: Ident,
    ty: Type,
}

struct AttributeDefinitions {
    entries: Vec<AttributeDefinition>,
}

impl Parse for AttributeDefinitions {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let mut entries = Vec::new();
        while !input.is_empty() {
            let _ = input.call(Attribute::parse_outer)?;
            if input.peek(Token![pub]) {
                let v: Visibility = input.parse()?;
                return Err(syn::Error::new_spanned(
                    v,
                    "visibility must appear after `as` and before the attribute name (e.g. `\"...\" as pub name: Type;`)",
                ));
            }

            let id: LitStr = input.parse()?;
            input.parse::<Token![as]>()?;
            if input.peek(Token![pub]) {
                let _: Visibility = input.parse()?;
            }
            let name: Ident = input.parse()?;
            input.parse::<Token![:]>()?;
            let ty: Type = input.parse()?;
            input.parse::<Token![;]>()?;

            entries.push(AttributeDefinition { id, name, ty });
        }
        Ok(AttributeDefinitions { entries })
    }
}

fn emit_attribute_definitions(context: &mut MetadataContext<'_>) {
    use triblespace_core::metadata;
    use triblespace_core::prelude::ValueSchema;
    use triblespace_core::value::schemas::genid::GenId;

    let Ok(parsed) =
        syn::parse2::<AttributeDefinitions>(TokenStream2::from(context.tokens().clone()))
    else {
        return;
    };
    if parsed.entries.is_empty() {
        return;
    }

    let invocation_hex = format!("{:X}", context.invocation_id());

    for definition in parsed.entries {
        let entity = fucid();

        // Parse the attribute hex ID into a proper Id for GenId storage.
        let Some(attr_id) = Id::from_hex(&definition.id.value()) else {
            continue;
        };

        let name_handle = context.workspace().put(definition.name.to_string());
        let mut set = ::triblespace_core::macros::entity! {
            &entity @
            metadata::attribute: GenId::value_from(attr_id),
            metadata::name: name_handle,
            metadata::tag: metadata::KIND_ATTRIBUTE_USAGE,
            attribute::invocation: invocation_hex.as_str()
        };

        let ty_tokens = definition.ty.to_token_stream().to_string();
        if !ty_tokens.is_empty() {
            let handle = context.workspace().put(ty_tokens);
            set +=
                ::triblespace_core::macros::entity! { &entity @ attribute::attribute_type: handle };
        }

        context.workspace().commit(set, "macro invocation");
    }
}

#[proc_macro]
pub fn attributes(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("attributes", &clone, |context| {
        emit_attribute_definitions(context)
    });
    let base_path: TokenStream2 = quote!(::triblespace::core);
    let tokens = TokenStream2::from(input);
    match attributes_impl(tokens, &base_path) {
        Ok(ts) => TokenStream::from(ts),
        Err(e) => e.to_compile_error().into(),
    }
}

#[proc_macro]
pub fn path(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("path", &clone, |_context| {});
    let base_path: TokenStream2 = quote!(::triblespace::core);
    let tokens = TokenStream2::from(input);
    match path_impl(tokens, &base_path) {
        Ok(ts) => TokenStream::from(ts),
        Err(e) => e.to_compile_error().into(),
    }
}

#[proc_macro]
pub fn pattern(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("pattern", &clone, |_context| {});
    let base_path: TokenStream2 = quote!(::triblespace::core);
    let tokens = TokenStream2::from(input);
    match pattern_impl(tokens, &base_path) {
        Ok(ts) => TokenStream::from(ts),
        Err(e) => e.to_compile_error().into(),
    }
}

#[proc_macro]
pub fn pattern_changes(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("pattern_changes", &clone, |_context| {});
    let base_path: TokenStream2 = quote!(::triblespace::core);
    let tokens = TokenStream2::from(input);
    match pattern_changes_impl(tokens, &base_path) {
        Ok(ts) => TokenStream::from(ts),
        Err(e) => e.to_compile_error().into(),
    }
}

#[proc_macro]
pub fn entity(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("entity", &clone, |_context| {});
    let base_path: TokenStream2 = quote!(::triblespace::core);
    let tokens = TokenStream2::from(input);
    match entity_impl(tokens, &base_path) {
        Ok(ts) => TokenStream::from(ts),
        Err(e) => e.to_compile_error().into(),
    }
}

#[proc_macro]
pub fn find(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("find", &clone, |_context| {});
    let inner = TokenStream2::from(input);
    TokenStream::from(quote!(::triblespace::core::macros::find!(#inner)))
}

#[proc_macro]
pub fn exists(input: TokenStream) -> TokenStream {
    let clone = input.clone();
    emit_metadata("exists", &clone, |_context| {});
    let inner = TokenStream2::from(input);
    TokenStream::from(quote!(::triblespace::core::exists!(#inner)))
}

#[proc_macro_attribute]
pub fn value_formatter(attr: TokenStream, item: TokenStream) -> TokenStream {
    let clone = item.clone();
    emit_metadata("value_formatter", &clone, |_context| {});

    match value_formatter_impl(TokenStream2::from(attr), TokenStream2::from(item)) {
        Ok(tokens) => TokenStream::from(tokens),
        Err(err) => err.to_compile_error().into(),
    }
}