symbolique 0.1.1

Symbol table pipeline for language servers — parse, link, merge, and resolve symbols across files, built on the laburnum LSP framework.
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! Record types for symbolique partitions.
//!
//! This module defines the record types that are stored in symbolique's
//! partitions. These types separate shape data (content-addressed, no spans)
//! from index entries (path-keyed, with spans).
//!
//! # Architecture (ADR0003)
//!
//! - **`Symbols<V, I, P, S>`**: Single CAS partition for all symbol shapes
//! - **`SymbolEntry<V, I, P, S>`**: Index entry pointing to shapes in `Symbols`
//! - **`ResolutionEntry<V, I, P, S>`**: Index entry for resolution mappings
//!
//! Stage partitions (FileSymbols, LinkedSymbols, MergedSymbols) are now
//! index-only, using `SymbolEntry` as their `IndexEntry` type.

use {
  crate::core::{Ident, SymbolPath, SymbolVisibility, Value, Visibility},
  laburnum::{
    ContentHash,
    database::{RecordHandle, partitions::IndexEntry},
    record::CollectReferences,
  },
  std::hash::Hash,
};

use super::symbols::Symbols;

/// Index entry for symbol occurrences in stage partitions.
///
/// Used by FileSymbols, LinkedSymbols, and MergedSymbols partitions as their
/// `IndexEntry` type. Links a path location to a symbol shape in the shared
/// `Symbols` partition.
///
/// # Type Parameters
///
/// - `V`: Value type for literals
/// - `I`: Identifier type for names
/// - `P`: Symbol path type
/// - `S`: Visibility type carried by definitions
///
/// # GC Role
///
/// Index entries are GC roots. The `symbol` field references a CAS record
/// in the `Symbols` partition, keeping it alive via reference counting.
#[derive(Clone)]
pub struct SymbolEntry<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// Path of this symbol occurrence.
  ///
  /// Retained (like [`ResolutionEntry::target_path`]) so consumers can recover
  /// each path segment's text via its span — the sort key alone keys user
  /// identifiers by hash, which is not reversible to source text.
  pub path: P,
  /// Source location of this symbol occurrence
  pub span: laburnum::Span,
  /// Handle to the content-addressed shape record in `Symbols` partition
  pub symbol: RecordHandle<Symbols<V, I, P, S>>,
}

impl<V, I, P, S> Hash for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    self.path.hash(state);
    self.span.hash(state);
    self.symbol.hash(state);
  }
}

impl<V, I, P, S> PartialEq for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn eq(&self, other: &Self) -> bool {
    self.path == other.path
      && self.span == other.span
      && self.symbol == other.symbol
  }
}

impl<V, I, P, S> Eq for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
}

impl<V, I, P, S> SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// Create a new symbol entry.
  pub fn new(
    path: P,
    span: laburnum::Span,
    symbol: RecordHandle<Symbols<V, I, P, S>>,
  ) -> Self {
    Self { path, span, symbol }
  }
}

impl<V, I, P, S> IndexEntry for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn primary_hash(&self) -> Option<ContentHash> {
    Some(self.symbol.content_hash())
  }
}

impl<V, I, P, S> laburnum::database::partitions::IndexedEntry
  for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn content_hash(&self) -> ContentHash {
    self.symbol.content_hash()
  }
}

impl<V, I, P, S> bluegum::Bluegum for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn node(&self, b: &mut bluegum::Builder) {
    b.name("SymbolEntry")
      .field("span", self.span)
      .field("symbol", self.symbol.content_hash());
  }
}

impl<V, I, P, S> bluegum::BluegumWithState<dyn laburnum::SourceResolver>
  for SymbolEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn node_with_state(
    &self,
    b: &mut bluegum::Builder,
    _state: &dyn laburnum::SourceResolver,
  ) {
    b.name("SymbolEntry")
      .field("symbol", self.symbol.content_hash());
  }
}

impl<Ps, V, I, P, S> CollectReferences<Ps> for SymbolEntry<V, I, P, S>
where
  Ps: laburnum::database::storage::Partitions,
  Ps::Stores: laburnum::database::partitions::HasPartition<Symbols<V, I, P, S>>,
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn collect_references<R: laburnum::record::References<Ps>>(
    &self,
    refs: &mut R,
  ) {
    refs.add(self.symbol);
  }
}

/// Index entry for resolution mappings.
///
/// Used by the SymbolResolution partition as its `IndexEntry` type. Maps a
/// reference path to its resolved target definition in the `Symbols` partition.
///
/// # Type Parameters
///
/// - `V`: Value type for literals
/// - `I`: Identifier type for names
/// - `P`: Symbol path type
/// - `S`: Visibility type carried by definitions
///
/// # GC Role
///
/// Index entries are GC roots. The `target` field references a CAS record
/// in the `Symbols` partition, keeping it alive via reference counting.
#[derive(Clone, Hash)]
pub enum ResolutionTarget<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// Found and visible from the reference site.
  Resolved(RecordHandle<Symbols<V, I, P, S>>),
  /// Found, but the definition's visibility denies the reference site.
  /// The handle is retained so go-to-definition still works for a hidden
  /// target.
  Inaccessible(RecordHandle<Symbols<V, I, P, S>>),
  /// No definition exists for the reference path.
  NotFound,
}

impl<V, I, P, S> ResolutionTarget<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// The target definition handle, present when a definition was found
  /// (`Resolved` or `Inaccessible`).
  pub fn handle(&self) -> Option<RecordHandle<Symbols<V, I, P, S>>> {
    match self {
      | Self::Resolved(h) | Self::Inaccessible(h) => Some(*h),
      | Self::NotFound => None,
    }
  }

  /// Whether the reference resolved to a visible definition.
  pub fn is_resolved(&self) -> bool {
    matches!(self, Self::Resolved(_))
  }
}

/// Index entry for the `SymbolResolution` partition.
///
/// Maps a reference path to its resolution outcome: the path it was trying to
/// reach plus a [`ResolutionTarget`] capturing whether it resolved, was found
/// but hidden, or was not found.
///
/// # GC Role
///
/// Index entries are GC roots. A `Resolved`/`Inaccessible` `target` references
/// a CAS record in the `Symbols` partition, keeping it alive via reference
/// counting; a `NotFound` entry references nothing.
#[derive(Clone, Hash)]
pub struct ResolutionEntry<V, I, P, S = SymbolVisibility>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// Path of the target definition (or the path that failed to resolve).
  pub target_path: P,
  /// The resolution outcome.
  pub target: ResolutionTarget<V, I, P, S>,
}

impl<V, I, P, S> ResolutionEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  /// Create a resolution entry from an explicit outcome.
  pub fn new(target_path: P, target: ResolutionTarget<V, I, P, S>) -> Self {
    Self {
      target_path,
      target,
    }
  }

  /// A reference that resolved to a visible definition.
  pub fn resolved(
    target_path: P,
    target: RecordHandle<Symbols<V, I, P, S>>,
  ) -> Self {
    Self::new(target_path, ResolutionTarget::Resolved(target))
  }

  /// A reference whose target was found but is not visible from the site.
  pub fn inaccessible(
    target_path: P,
    target: RecordHandle<Symbols<V, I, P, S>>,
  ) -> Self {
    Self::new(target_path, ResolutionTarget::Inaccessible(target))
  }

  /// A reference with no matching definition.
  pub fn not_found(target_path: P) -> Self {
    Self::new(target_path, ResolutionTarget::NotFound)
  }
}

impl<V, I, P, S> IndexEntry for ResolutionEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn primary_hash(&self) -> Option<ContentHash> {
    // GC root only for outcomes that reference a real CAS record; a NotFound
    // entry keeps nothing alive.
    self.target.handle().map(|h| h.content_hash())
  }
}

impl<V, I, P, S> laburnum::database::partitions::IndexedEntry
  for ResolutionEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn content_hash(&self) -> ContentHash {
    // Identity of the resolution itself — target path plus outcome (kind and
    // handle). A NotFound entry references no CAS record, so its identity is
    // derived from its own content; this also makes a Resolved→Inaccessible
    // flip a content change even when the target handle is unchanged.
    laburnum::record::hash_record(self)
  }
}

impl<V, I, P, S> bluegum::Bluegum for ResolutionEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn node(&self, b: &mut bluegum::Builder) {
    // No resolver here and the path key has no Debug/text form;
    // node_with_state renders it via resolve().
    b.name("ResolutionEntry").field("target_path", "<path>");
    match &self.target {
      | ResolutionTarget::Resolved(h) => {
        b.field("resolved", h.content_hash());
      },
      | ResolutionTarget::Inaccessible(h) => {
        b.field("inaccessible", h.content_hash());
      },
      | ResolutionTarget::NotFound => {
        b.field("kind", format!("{:?}", "NotFound"));
      },
    }
  }
}

impl<V, I, P, S> bluegum::BluegumWithState<dyn laburnum::SourceResolver>
  for ResolutionEntry<V, I, P, S>
where
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
}

impl<Ps, V, I, P, S> CollectReferences<Ps> for ResolutionEntry<V, I, P, S>
where
  Ps: laburnum::database::storage::Partitions,
  Ps::Stores: laburnum::database::partitions::HasPartition<Symbols<V, I, P, S>>,
  V: Value<I>,
  I: Ident,
  P: SymbolPath,
  S: Visibility,
{
  fn collect_references<R: laburnum::record::References<Ps>>(
    &self,
    refs: &mut R,
  ) {
    if let Some(handle) = self.target.handle() {
      refs.add(handle);
    }
  }
}