laburnum 1.17.0

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

// -- AST Macros ---------------------------------------------------------------

/// Define the `Node` enum for an AST crate.
///
/// This macro generates:
/// - A `Node` enum with a variant for each registered node type
/// - `bluegum::Bluegum` and `BluegumWithState<Printer>` impls for visualization
/// - Helper methods: `get_id()`, `validate()`, `get_node_name()`, `get_syntax()`
/// - Match helpers and `from_node()` for each variant
///
/// # Convention
///
/// The consuming crate must:
/// - Re-export the CST crate as `crate::cst` (e.g., `pub use patina_syntax_concrete as cst;`)
/// - Define `crate::Printer`, `crate::AST`, `crate::NodeId`, `crate::style_title()`
/// - Define `crate::Result` type alias
///
/// # Example
///
/// ```ignore
/// laburnum::define_node_enum! { parser::State =>
///     crate::Error,
///     crate::symbol::Ident,
///     crate::literal::IntLiteral,
/// }
/// ```
#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! define_node_enum {
  (
    // parser state
    $($parser_state:ident)::+
       =>
      // variants
      $( crate::$($node_name:ident)::+ ),* $(,)?
  ) => {
    paste::paste! {
      #[derive(Debug, Clone, PartialEq)]
      pub enum Node {
        $([< $($node_name:camel)+ >](
              crate::$($node_name)::+
          ),
        )*
      }

      impl bluegum::Bluegum for Node {
        fn node(&self, b: &mut bluegum::Builder) {
          b.name("Node")
            .field("id", format!("{:?}",&self))
            .debug("Help", "For the full node use `node_with_state`");
        }
      }

      impl<'nodes>
        bluegum::BluegumWithState<crate::Printer<'nodes>>
        for Node {
          #[allow(unused)]
        fn node_with_state(
          &self,
          b: &mut bluegum::Builder,
          state: &crate::Printer<'nodes>,
        ) {
          (state.pre_callback)(b, self);

          match self {
            $(
             | Node :: [< $($node_name:camel)+ >](node) => {
                  node.node_with_state(b,state);
                }
            ),*
            | _ => {
              b.name("Node");
            }
          }

          (state.post_callback)(b, self);
        }
      }

      impl Node {
        #[allow(unused)]
        pub fn get_id(
          &self,
        ) -> crate::NodeId {
          match self {
            $(
             | Node :: [< $($node_name:camel)+ >](node) => {
                  node.get_id()
                }
            ),*
          }
        }
      }

      #[allow(unused)]
      impl Node {

       #[allow(clippy::result_unit_err)]
        pub fn validate(
          &self,
          ast: &crate::AST,
        ) -> crate::Result<()> {
          match self {
            $(
              | Node :: [< $($node_name:camel)+ >](node) => node.validate(ast),
            )*
            | _ => {
              Ok(())
            }
          }
        }

        /// Get the name of the AST Node
        pub fn get_node_name(&self) -> String {
          match self {
            $(
              | Node :: [< $($node_name:camel)+ >](node) => stringify!([<$($node_name:camel)+>]).to_string(),
            )*
          }
        }

        pub fn get_syntax(&self) -> crate::cst::CstNodeId {
          match self {
            $(
              | Node :: [< $($node_name:camel)+ >](node) => node.get_syntax(),
            )*
          }
        }


        #[allow(unused)]
        $(
          pub fn [< match_ $($node_name:snake)_+ >]
            (
            &self,
            ast: &crate::AST,
          ) -> Option<& crate::$($node_name)::+ >{
            match &self {
              | crate::Node :: [< $($node_name:camel)+  >](node) => Some(node),
              | _ => {
                None
              }
            }
          }
        )*

      }

      //---
    $(
      impl crate::$($node_name)::+ {
        pub fn from_node(node: &crate::Node) -> Option<& crate::$($node_name)::+ > {
          match node {
            | crate::Node :: [< $($node_name:camel)+  >](node) => Some(node),
            | _ => {
              None
            }
          }
        }
      }
    )*

    }
  };
}

/// Parse a group of CST nodes matching the given delimiter and separator.
///
/// # Convention
///
/// The consuming crate must re-export the CST crate as `crate::cst`.
///
/// # Example
///
/// ```ignore
/// let items = laburnum::group!(
///     state, body_syntax,
///     delimiter: Brace,
///     separator: Comma,
///     => handler_fn
/// );
/// ```
#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! group {
  (@option delimiter None) => {
    None
  };

  (@option delimiter $Delimiter:ident) => {
    Some(crate::cst::group::DelimitedBy::$Delimiter)
  };

  (@option separator None) => {
    None
  };

  (@option separator $Separator:ident) => {
    Some(crate::cst::group::SeparatedBy::$Separator)
  };

  (
    $state:ident,
    $syntax:ident,
    $(delimiter: $Delimiter:ident , )?
    $(separator: $Separator:ident , )?
    $(leading_separator: $leading_separator:expr , )?
    $(trailing_separator: $trailing_separator:expr , )?
    => $handler:path
  ) => {{
    if let Some(crate::cst::Node::Group{
      node: crate::cst::Group {
      $( delimiter: $crate::group!(@option delimiter $Delimiter), )?
      $( separator: $crate::group!(@option separator $Separator), )?
      $(leading_separator:  $leading_separator, )?
      $(trailing_separator:  $trailing_separator, )?
      nodes,
      ..
    },
    ..
    }) = $state.load(&$syntax) {
          nodes
            .get()
            .iter()
            .map(|syntax| $handler($state, *syntax))
            .collect::<Vec<_>>()
    }
    else {
      vec![$handler($state, $syntax)]
    }
  }};
}

/// Optionally parse a group of CST nodes matching the given delimiter and separator.
///
/// Returns `Option<Vec<NodeId>>`. Returns `None` if the node doesn't match.
///
/// # Convention
///
/// The consuming crate must re-export the CST crate as `crate::cst`.
///
/// # Example
///
/// ```ignore
/// let items = laburnum::try_group!(
///     state, node,
///     delimiter: Bracket,
///     separator: Comma,
///     allow_empty: true,
///     => handler_fn
/// );
/// ```
#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! try_group {
  (@allow_empty true ) => { Some(vec![]) as Option<Vec<_>> };

  (@allow_empty) => { None as Option<Vec<_>> };

  (
    $state:ident,
    $node:ident,
    $(delimiter: $Delimiter:ident , )?
    $(separator: $Separator:ident , )?
    $(leading_separator: $leading_separator:expr , )?
    $(trailing_separator: $trailing_separator:expr , )?
    $(allow_empty:  $allow_empty:ident   , )?
    => $handler:path
  ) => {{
   match $node  {
     | crate::cst::Node::Group{
        node: crate::cst::Group {
          $( delimiter: $crate::group!(@option delimiter $Delimiter), )?
          $( separator: $crate::group!(@option separator $Separator), )?
          is_empty: true,
          ..
        },
        ..
      } => {
       $crate::try_group!(
         @allow_empty $( $allow_empty )?)
     },
     | crate::cst::Node::Group{
        node: crate::cst::Group {
          $( delimiter: $crate::group!(@option delimiter $Delimiter), )?
          $( separator: $crate::group!(@option separator $Separator), )?
          $(leading_separator:  $leading_separator, )?
          $(trailing_separator:  $trailing_separator, )?
          is_empty: false,
          nodes,
          ..
        },
      ..
     } => {
          Some(nodes
            .get()
            .iter()
            .map(|syntax| $handler($state, *syntax))
            .collect::<Vec<_>>())
      },
      | _ => None as Option<Vec<_>>,
   }
  }};
}

// -- Lower --------------------------------------------------------------------

/// Configuration trait for CST-to-AST lowering.
///
/// Implement this trait to define the concrete types used by your language's
/// lowering pass. This allows [`Lower`] and [`LowerMany`] to be generic across
/// different language implementations.
///
/// # Example
///
/// ```ignore
/// struct MyCfg;
///
/// impl laburnum::chumsky::ast::LowerConfig for MyCfg {
///   type State = MyParserState;
///   type SyntaxId = MyCstNodeId;
///   type SyntaxNode = MyCstNode;
///   type NodeId = MyAstNodeId;
/// }
/// ```
pub trait LowerConfig {
  /// The mutable parser state threaded through lowering.
  type State;

  /// The ID type used to reference CST nodes.
  type SyntaxId: Copy;

  /// The CST node type.
  type SyntaxNode;

  /// The ID type used to reference AST nodes.
  type NodeId;
}

/// Lower a CST node into an AST node.
pub trait Lower<C: LowerConfig> {
  /// Lower a CST node into an AST node.
  /// If the node is not valid, create an error Node.
  fn must_parse(state: &mut C::State, syntax: C::SyntaxId) -> C::NodeId;

  /// Optionally Lower a CST node into an AST node.
  /// Useful for chaining multiple optional parsers.
  fn try_parse(
    state: &mut C::State,
    syntax: C::SyntaxId,
    node: &C::SyntaxNode,
  ) -> Option<C::NodeId>;
}

/// Lower a CST node into a vector of AST nodes.
pub trait LowerMany<C: LowerConfig> {
  /// Lower a CST node into a vector of AST nodes.
  /// If the node is not valid, create an error Node.
  fn must_parse_many(
    state: &mut C::State,
    syntax: C::SyntaxId,
  ) -> Vec<C::NodeId>;

  /// Optionally Lower a CST node into a vector of AST nodes.
  /// Useful for chaining multiple optional parsers.
  fn try_parse_many(
    state: &mut C::State,
    syntax: C::SyntaxId,
    node: &C::SyntaxNode,
  ) -> Option<Vec<C::NodeId>>;
}