laburnum 1.17.2

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

/// Macro for defining compile-time watcher handlers using PK-based dispatch.
///
/// Implements the KeyWatcher trait for the specified struct, generating a
/// struct that holds handler lists per PK, with each handler optionally having
/// glob filtering.
///
/// # Syntax
///
/// ```ignore
/// watchers! {
///   (
///     Server: MyServer,
///     Storage: MyStorage,
///   ),
///   pk => handler_function,
///   pk => another_handler("*.rs", "*.toml"),
/// }
/// ```
///
/// # Example
///
/// ```ignore
/// watchers! {
///   (
///     Server: MyLanguageServer,
///     Storage: MyStorage,
///   ),
///   MyPartition::KEY => on_symbol_change,
///   ConfigPartition::KEY => on_config_change("*.rs"),
/// }
///
/// async fn on_symbol_change<P: Partitions, T: LanguageServer<P>>(
///   ctx: &mut TaskContext<P, T>,
///   writer: &mut PartitionWriteContextRef<'_, P>,
/// ) {
///   // Each key is a RecordKey with opaque partition_key + sort_key
///   for key in ctx.matched_keys_updated() {
///     let results = key.get_record(ctx.query_client()).await;
///     // process results...
///   }
/// }
/// ```
#[macro_export]
macro_rules! watchers {
  // Entry point. Forwards the entry list verbatim to the per-entry muncher,
  // which handles each entry's optional `per_key` modifier and optional glob
  // filters independently (so entries may freely mix the four forms).
  //
  // Per-entry forms:
  //   PK::KEY => handler,
  //   PK::KEY => handler("*.rs", "*.toml"),
  //   PK::KEY => per_key handler,
  //   PK::KEY => per_key handler("*.rs"),
  //
  // `per_key` dispatches the handler once per changed sort key (each its own
  // task and commit) instead of once per commit batch — for watchers whose
  // per-record work is independent. See GLD-0035.
  (
    (
      Server: $server:ident,
      Storage: $storage:ty $(,)?
    ),
    $($rest:tt)*
  ) => {
    $crate::watchers!(@expand_entries $server, [$storage] [] $($rest)*);
  };



  // Expand entries - base case: all entries expanded, now continue to impl (with storage bounds)
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*]) => {
    $crate::watchers!(@impl $struct_name, [$storage_ty]
      // User-defined watchers
      $($expanded)*
    );
  };

  // The four per-entry forms. Order matters: `per_key` arms precede their
  // batch counterparts, and glob arms precede their no-glob counterparts, so
  // the most specific pattern wins. Each normalises into the accumulator as
  // `[$pk] => $handler [$path] ($per_key) [$globs?],`.

  // per_key + globs
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => per_key $handler:ident ($($glob:literal),+ $(,)?) $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler] (true) [$($glob),+],
    ] $($($rest)*)?);
  };

  // per_key + no globs
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => per_key $handler:ident $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler] (true),
    ] $($($rest)*)?);
  };

  // batch + globs
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => $handler:ident ($($glob:literal),+ $(,)?) $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler] (false) [$($glob),+],
    ] $($($rest)*)?);
  };

  // batch + no globs
  (@expand_entries $struct_name:ident, [$storage_ty:ty] [$($expanded:tt)*] $pk:path => $handler:ident $(, $($rest:tt)*)?) => {
    $crate::watchers!(@expand_entries $struct_name, [$storage_ty] [
      $($expanded)*
      [$pk] => $handler [$handler] (false),
    ] $($($rest)*)?);
  };

  // Implementation - collect all handlers and generate structs (with storage bounds)
  (@impl $struct_name:ident, [$storage_ty:ty] $($handlers:tt)*) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [] $($handlers)*);
  };

  // Collect handlers - base case, forward to generate (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*]) => {
    $crate::watchers!(@generate_all $struct_name, [$storage_ty] $($grouped)*);
  };

  // Collect handlers - with globs (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*] [$pk:path] => $handler:ident [$handler_path:path] ($per_key:ident) [$($glob:literal),+] $(, $($rest:tt)*)?) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [
      $($grouped)*
      ([$pk], $handler, $handler_path, $per_key, [$($glob),+])
    ] $($($rest)*)?);
  };

  // Collect handlers - without globs (with storage bounds)
  (@collect_handlers $struct_name:ident, [$storage_ty:ty] [$($grouped:tt)*] [$pk:path] => $handler:ident [$handler_path:path] ($per_key:ident) $(, $($rest:tt)*)?) => {
    $crate::watchers!(@collect_handlers $struct_name, [$storage_ty] [
      $($grouped)*
      ([$pk], $handler, $handler_path, $per_key)
    ] $($($rest)*)?);
  };

  // Generate all structures and impl - takes tuples: ([pk], handler_name, handler_path, per_key, optional_glob_array) (with storage bounds)
  (@generate_all $struct_name:ident, [$storage_ty:ty] $(([$pk:path], $handler:ident, $handler_path:path, $per_key:ident $(, [$($glob:literal),+])?)) *) => {
    paste::paste! {
      struct [<$struct_name HandlerMeta>] {
        task_id: $crate::Ident,
        pk: $crate::Ident,
        /// Dispatch this handler once per changed sort key (GLD-0035) rather
        /// than once per commit batch.
        per_key: bool,
        sk_globset: Option<globset::GlobSet>,
      }

      struct [<$struct_name Meta>] {
        $(
          [<$handler:snake>]: [<$struct_name HandlerMeta>],
        )*
      }

      static [<$struct_name:upper _META>]: std::sync::LazyLock<[<$struct_name Meta>]> = std::sync::LazyLock::new(|| {
        [<$struct_name Meta>] {
          $(
            [<$handler:snake>]: [<$struct_name HandlerMeta>] {
              task_id: $crate::Ident::new(concat!("watcher:", stringify!($pk), ":", stringify!($handler))),
              pk: $pk,
              per_key: $per_key,
              sk_globset: $crate::watchers!(@build_globset $handler, $([$($glob),+])?),
            },
          )*
        }
      });

      impl $crate::scheduler::key_watcher::KeyWatcher<$storage_ty, $struct_name> for $struct_name
      {
        fn dispatch_watcher<F>(
          pk: $crate::Ident,
          updated_sks: Vec<$crate::database::RecordKey<$storage_ty>>,
          deleted_sks: Vec<$crate::database::RecordKey<$storage_ty>>,
          spawn: F,
        )
        where
          F: Fn(
            $crate::Ident,
            Vec<$crate::database::RecordKey<$storage_ty>>,
            Vec<$crate::database::RecordKey<$storage_ty>>,
            for<'a> fn(
              &'a mut $crate::scheduler::task::TaskContext<$storage_ty, $struct_name>,
              &'a mut $crate::database::PartitionWriteContextRef<'a, $storage_ty>)
                -> std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = $crate::scheduler::key_watcher::WatcherResult<$storage_ty, $struct_name>> + std::marker::Send + 'a>>),
        {
          $(
            if pk == [<$struct_name:upper _META>].[<$handler:snake>].pk {
              $crate::watchers!(@spawn_filtered $struct_name, pk, updated_sks, deleted_sks, spawn, $handler, $handler_path $(, [$($glob),+])?);
            }
          )*
        }
      }
    }
  };

  // Spawn with filtering - no globs (spawn all keys)
  (@spawn_filtered $struct_name:ty, $pk:ident, $updated_sks:ident, $deleted_sks:ident, $spawn:ident, $handler:ident, $handler_path:path) => {
    paste::paste! {
      if !$updated_sks.is_empty() || !$deleted_sks.is_empty() {
        let meta = &[<$struct_name:upper _META>].[<$handler:snake>];
        if meta.per_key {
          // One spawn per changed key — each runs as its own task/commit.
          for sk in &$updated_sks {
            $spawn(meta.pk.clone(), std::vec![sk.clone()], std::vec::Vec::new(), $handler_path);
          }
          for sk in &$deleted_sks {
            $spawn(meta.pk.clone(), std::vec::Vec::new(), std::vec![sk.clone()], $handler_path);
          }
        } else {
          $spawn(meta.pk.clone(), $updated_sks.clone(), $deleted_sks.clone(), $handler_path);
        }
      }
    }
  };

  // Spawn with filtering - with globs
  (@spawn_filtered $struct_name:ty, $pk:ident, $updated_sks:ident, $deleted_sks:ident, $spawn:ident, $handler:ident, $handler_path:path, [$($glob:literal),+]) => {
    paste::paste! {
      {
        let meta = &[<$struct_name:upper _META>].[<$handler:snake>];
        let Some(glob_set) = meta.sk_globset.as_ref() else {
          // No glob set configured - skip filtering
          return;
        };
        let mut filtered_updated = $updated_sks.clone();
        let mut filtered_deleted = $deleted_sks.clone();
        filtered_updated.retain(|sk| glob_set.is_match(sk));
        filtered_deleted.retain(|sk| glob_set.is_match(sk));
        if !filtered_updated.is_empty() || !filtered_deleted.is_empty() {
          if meta.per_key {
            for sk in &filtered_updated {
              $spawn(meta.pk.clone(), std::vec![sk.clone()], std::vec::Vec::new(), $handler_path);
            }
            for sk in &filtered_deleted {
              $spawn(meta.pk.clone(), std::vec::Vec::new(), std::vec![sk.clone()], $handler_path);
            }
          } else {
            $spawn(meta.pk.clone(), filtered_updated, filtered_deleted, $handler_path);
          }
        }
      }
    }
  };

  // Helper: create optional GlobSet for LazyLock initialization
  (@build_globset $handler:ident,) => { None };
  (@build_globset $handler:ident, [$($glob:literal),+]) => {
    {
      let mut builder = globset::GlobSetBuilder::new();
      $(
        match globset::Glob::new($glob) {
          | Ok(glob) => { builder.add(glob); },
          | Err(e) => {
            eprintln!("Warning: Invalid glob pattern `{}` in handler `{}`: {}", $glob, stringify!($handler), e);
          }
        }
      )+
      match builder.build() {
        | Ok(globset) => Some(globset),
        | Err(e) => {
          eprintln!("Warning: Failed to build GlobSet for handler `{}`: {}", stringify!($handler), e);
          None
        }
      }
    }
  };

  // Helper: create optional GlobSet (legacy)
  (@make_globset) => { None };
  (@make_globset $($glob:literal),+) => {
    {
      let mut builder = globset::GlobSetBuilder::new();
      $(
        match globset::Glob::new($glob) {
          | Ok(glob) => { builder.add(glob); },
          | Err(e) => {
            eprintln!("Warning: Invalid glob pattern `{}`: {}", $glob, e);
          }
        }
      )+
      match builder.build() {
        | Ok(globset) => Some(globset),
        | Err(e) => {
          eprintln!("Warning: Failed to build GlobSet: {}", e);
          None
        }
      }
    }
  };
}