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

use crate::{
  SourceKey,
  protocol::lsp,
};

crate::define_dyn_partition!(
  /// To write diagnostics, create a wrapper partition with [`impl_partition_for_dyn!`]:
  ///
  /// ```rust,ignore
  /// use laburnum::{impl_partition_for_dyn, partitions::Diagnostics};
  ///
  /// // Your record type must implement DiagnosticRecord + Record
  /// impl_partition_for_dyn!(MyDiagnosticsPartition, Diagnostics, MyDiagnostic);
  ///
  /// // Then write using the wrapper partition
  /// let sort_key = DiagnosticSortKey::Diagnostic { source_key, stage, severity, sequence };
  /// writer.write::<MyDiagnosticsPartition>(sort_key, diagnostic.into());
  /// ```
  ///
  /// # Clearing Old Diagnostics
  ///
  /// When re-processing a file, clear old diagnostics before writing new ones using
  /// [`PartitionWriteContext::clear_prefix`]. A producer should clear only the
  /// diagnostics it owns via [`DiagnosticSortKey::SourceStagePrefix`], so it does
  /// not wipe diagnostics emitted by other stages for the same file:
  ///
  /// ```rust,ignore
  /// // Clear this producer's diagnostics for the file before re-emitting
  /// writer.clear_prefix(
  ///   Diagnostics::KEY,
  ///   DiagnosticSortKey::SourceStagePrefix(source_key, MY_STAGE),
  /// );
  ///
  /// // Then write new diagnostics (or none if the file is valid)
  /// for error in errors {
  ///   writer.write::<MyDiagnosticsPartition>(sort_key, diagnostic);
  /// }
  /// ```
  ///
  /// This ensures that when a file is fixed (no more errors), the old diagnostics
  /// are cleared and watchers are notified to send empty diagnostics to the client.
  ///
  /// [`impl_partition_for_dyn!`]: crate::impl_partition_for_dyn
  /// [`PartitionWriteContext::clear_prefix`]: crate::database::PartitionWriteContext::clear_prefix
  Diagnostics,
  "laburnum::diagnostics",
  DiagnosticSortKey,
  DiagnosticRecord
);

#[derive(Debug, Clone)]
pub enum DiagnosticSortKey {
  /// A source-level diagnostic: a stage that owns the whole file's
  /// diagnostics for its namespace (lexer, parser, coherence, ...).
  Diagnostic {
    source_key: SourceKey,
    /// Producer namespace. Diagnostics for one file are partitioned by the
    /// pipeline stage that emitted them (lexer, parser, type resolution,
    /// references, ...) so each producer can clear and re-emit only its own
    /// diagnostics via [`DiagnosticSortKey::SourceStagePrefix`] without
    /// disturbing the others. The meaning of the value is owned by the
    /// caller; laburnum treats it as an opaque ordering segment.
    stage:      u16,
    severity:   lsp::DiagnosticSeverity,
    sequence:   u16,
  },
  /// A per-declaration diagnostic: a stage that runs once per declaration
  /// (e.g. per-declaration type resolution dispatched per-key) sets `sort_key`
  /// to the declaration's own sort key — an opaque, caller-provided byte
  /// encoding — so it can clear and re-emit only its own diagnostics via
  /// [`DiagnosticSortKey::SourceStageDeclPrefix`] without clobbering sibling
  /// declarations in the same file. `sort_key` is rendered as a hex run
  /// terminated by `|`; hex never contains `|`, so a per-declaration prefix
  /// never bleeds into another declaration's key or the `severity` word that
  /// follows in a source-level [`Diagnostic`] key. The encoding is opaque to
  /// laburnum and must be injective and stable across runs — the caller owns
  /// its meaning (e.g. a Bullion `SymbolPath`).
  DiagnosticForDecl {
    source_key: SourceKey,
    stage:      u16,
    sort_key:   Box<[u8]>,
    severity:   lsp::DiagnosticSeverity,
    sequence:   u16,
  },
  All,
  FilePrefix(u16),
  /// All diagnostics for a file, across every producer stage.
  SourcePrefix(SourceKey),
  /// All diagnostics for a file emitted by a single producer stage (covering
  /// both source-level and per-declaration keys of that stage). Use this when
  /// a stage owns the whole file's diagnostics (source-level producers, or a
  /// batch re-validation).
  SourceStagePrefix(SourceKey, u16),
  /// All diagnostics for one declaration within a stage. Use this as the
  /// `clear_prefix` target when a per-declaration stage re-runs, so it clears
  /// only the diagnostics it owns and leaves sibling declarations untouched.
  SourceStageDeclPrefix(SourceKey, u16, Box<[u8]>),
}

impl std::fmt::Display for DiagnosticSortKey {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      | DiagnosticSortKey::Diagnostic {
        source_key,
        stage,
        severity,
        sequence,
      } => write!(f, "{}|{:05}|{}|{:04}", source_key, stage, severity, sequence),
      | DiagnosticSortKey::DiagnosticForDecl {
        source_key,
        stage,
        sort_key,
        severity,
        sequence,
      } => {
        write!(f, "{}|{:05}|", source_key, stage)?;
        for b in sort_key.iter() {
          write!(f, "{:02x}", b)?;
        }
        write!(f, "|{}|{:04}", severity, sequence)
      },
      | DiagnosticSortKey::All => Ok(()),
      | DiagnosticSortKey::FilePrefix(file_id) => {
        write!(f, "{}v", file_id)
      },
      | DiagnosticSortKey::SourcePrefix(source_key) => {
        write!(f, "{}|", source_key)
      },
      | DiagnosticSortKey::SourceStagePrefix(source_key, stage) => {
        write!(f, "{}|{:05}|", source_key, stage)
      },
      | DiagnosticSortKey::SourceStageDeclPrefix(source_key, stage, sort_key) => {
        write!(f, "{}|{:05}|", source_key, stage)?;
        for b in sort_key.iter() {
          write!(f, "{:02x}", b)?;
        }
        write!(f, "|")
      },
    }
  }
}

// -- Sort key ordering (ADR0011) ----------------------------------------------

/// Ordered components of a [`DiagnosticSortKey`].
///
/// Comparison and prefix containment are defined over this token sequence, so a
/// prefix variant (e.g. [`DiagnosticSortKey::SourceStagePrefix`]) is literally a
/// leading slice of the full keys it covers. `SourceKey` is split into `File` +
/// `Version` so `FilePrefix` spans every version of a file. The `Decl` token
/// sits before `Severity`, so per-declaration diagnostics sort within their
/// declaration without colliding with source-level ones at the same stage.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
enum DiagToken {
  File(u16),
  Version(u16),
  Stage(u16),
  Decl(Box<[u8]>),
  Severity(lsp::DiagnosticSeverity),
  Seq(u16),
}

impl DiagnosticSortKey {
  fn tokens(&self) -> Vec<DiagToken> {
    match self {
      | DiagnosticSortKey::All => Vec::new(),
      | DiagnosticSortKey::FilePrefix(file_id) => {
        vec![DiagToken::File(*file_id)]
      },
      | DiagnosticSortKey::SourcePrefix(sk) => {
        vec![DiagToken::File(sk.file_id()), DiagToken::Version(sk.version())]
      },
      | DiagnosticSortKey::SourceStagePrefix(sk, stage) => vec![
        DiagToken::File(sk.file_id()),
        DiagToken::Version(sk.version()),
        DiagToken::Stage(*stage),
      ],
      | DiagnosticSortKey::SourceStageDeclPrefix(sk, stage, sort_key) => vec![
        DiagToken::File(sk.file_id()),
        DiagToken::Version(sk.version()),
        DiagToken::Stage(*stage),
        DiagToken::Decl(sort_key.clone()),
      ],
      | DiagnosticSortKey::Diagnostic {
        source_key,
        stage,
        severity,
        sequence,
      } => vec![
        DiagToken::File(source_key.file_id()),
        DiagToken::Version(source_key.version()),
        DiagToken::Stage(*stage),
        DiagToken::Severity(*severity),
        DiagToken::Seq(*sequence),
      ],
      | DiagnosticSortKey::DiagnosticForDecl {
        source_key,
        stage,
        sort_key,
        severity,
        sequence,
      } => vec![
        DiagToken::File(source_key.file_id()),
        DiagToken::Version(source_key.version()),
        DiagToken::Stage(*stage),
        DiagToken::Decl(sort_key.clone()),
        DiagToken::Severity(*severity),
        DiagToken::Seq(*sequence),
      ],
    }
  }
}

impl PartialEq for DiagnosticSortKey {
  fn eq(&self, other: &Self) -> bool {
    self.tokens() == other.tokens()
  }
}
impl Eq for DiagnosticSortKey {}
impl std::hash::Hash for DiagnosticSortKey {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    self.tokens().hash(state);
  }
}
impl PartialOrd for DiagnosticSortKey {
  fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
    Some(self.cmp(other))
  }
}
impl Ord for DiagnosticSortKey {
  fn cmp(&self, other: &Self) -> std::cmp::Ordering {
    self.tokens().cmp(&other.tokens())
  }
}

impl crate::database::PartitionSortKey for DiagnosticSortKey {
  fn is_prefix_of(&self, other: &Self) -> bool {
    other.tokens().starts_with(&self.tokens())
  }

  // TODO(ADR0011 atomic flip): move the `Display` body here and delete the
  // `Display` impl once the macro/storage stop stringifying keys.
  fn resolve(&self, _resolver: &dyn crate::SourceResolver) -> String {
    self.to_string()
  }
}

pub trait DiagnosticRecord: Send + Sync + std::fmt::Debug {
  fn to_lsp_diagnostic(
    &self,
    source_cache: &crate::source::cache::reporter::SourceCacheReader,
    encoding: &crate::protocol::lsp::PositionEncodingKind,
  ) -> lsp::Diagnostic;

  fn to_ariadne_report(
    &self,
    source_cache: &crate::source::cache::reporter::SourceCacheReader,
  ) -> ariadne::Report<'static, (SourceKey, std::ops::Range<usize>)>;

  fn source_key(&self) -> Option<SourceKey>;

  fn severity(&self) -> Option<lsp::DiagnosticSeverity>;

  fn message(&self) -> Option<&str>;
}