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

//! Resolution driver.
//!
//! [`resolve_references`] is the generic loop that connects a
//! [`SymboliqueResolver`] to the partitions: it enumerates the references in
//! `FileSymbols`, resolves each through the resolver, writes the resolved
//! mappings to `SymbolResolution`, and returns the references that did not
//! resolve to an accessible definition so the caller can raise its own
//! diagnostics.
//!
//! [`QueryResolveCtx`] is the concrete [`ResolveCtx`] the driver hands to the
//! resolver: it looks definitions up in `FileSymbols` (a module is a file, so
//! every definition lives there at its module-prefixed path) and reads their
//! visibility from the `Symbols` content-addressed store. Languages normally
//! consume this rather than implementing [`ResolveCtx`] themselves.

use {
  crate::{
    core::{Ident, Symbol, SymbolPath, Value, Visibility},
    partitions::{
      FileSymbols, ResolutionWriteExt, SymbolEntry, SymbolResolution,
      SymboliqueReadExt, Symbols,
    },
    resolution::{ResolveCtx, Resolution, SymboliqueResolver},
  },
  laburnum::database::{
    HasPartition, PartitionWriteContextRef, query::QueryClient,
    storage::Partitions,
  },
  std::hash::Hash,
};

// -- QueryResolveCtx ----------------------------------------------------------

/// A [`ResolveCtx`] backed by a `QueryClient`.
///
/// Looks definitions up in `FileSymbols` (by sort key) and reads their
/// visibility by following the entry's handle into the `Symbols` store.
pub struct QueryResolveCtx<'q, P: Partitions> {
  query: &'q QueryClient<P>,
}

impl<'q, P: Partitions> QueryResolveCtx<'q, P> {
  pub fn new(query: &'q QueryClient<P>) -> Self {
    Self { query }
  }
}

impl<P, V, I, Path, S> ResolveCtx<V, I, Path, S> for QueryResolveCtx<'_, P>
where
  P: Partitions,
  V: Value<I> + 'static,
  I: Ident + Hash,
  Path: SymbolPath + 'static,
  S: Visibility,
  P::Stores: HasPartition<FileSymbols<V, I, Path, S>>
    + HasPartition<Symbols<V, I, Path, S>>,
{
  fn lookup(&self, path: &Path) -> Option<SymbolEntry<V, I, Path, S>> {
    // A module is a file, so every definition — same-module or cross-module —
    // lives in `FileSymbols` at its module-prefixed path. Cross-module access
    // is gated by `visibility_of` below, not a separate exports table.
    self
      .query
      .file_symbol_at_path::<V, I, Path, S>(path)
  }

  fn visibility_of(&self, entry: &SymbolEntry<V, I, Path, S>) -> Option<S> {
    let record = self.query.resolve_file_symbol::<V, I, Path, S>(entry)?;
    match record.record()? {
      | Symbol::Definition { visibility, .. } => Some(*visibility),
      | _ => None,
    }
  }
}

// -- ResolutionFailure --------------------------------------------------------

/// A reference that did not resolve to an accessible definition.
///
/// Returned by [`resolve_references`] so the language can map it to its own
/// diagnostic. `resolution` is [`Resolution::Unresolved`] (no such
/// definition) or [`Resolution::Inaccessible`] (found, but not visible from
/// the reference site).
pub struct ResolutionFailure<V, I, P, S>
where
  V: Value<I>,
  I: Ident + Hash,
  P: SymbolPath,
  S: Visibility,
{
  /// Path of the reference site.
  pub reference_path: P,
  /// Path the reference was trying to reach.
  pub target_path: P,
  /// Span of the reference occurrence (for diagnostics).
  pub span: laburnum::Span,
  /// Why it failed.
  pub resolution: Resolution<V, I, P, S>,
}

// -- resolve_references -------------------------------------------------------

/// Resolve every nameable reference and write the results to
/// `SymbolResolution`.
///
/// Enumerates the `Symbol::Reference` entries in `FileSymbols`, resolves each
/// through `resolver` (handed a [`QueryResolveCtx`] over `query`), and writes
/// the resolved reference → target mappings via [`ResolutionWriteExt`].
///
/// Returns the references that did not resolve to an accessible definition
/// (unresolved or inaccessible); the driver raises no diagnostics itself.
/// Deferred references are skipped. Callers that re-resolve incrementally
/// should clear the relevant `SymbolResolution` prefix beforehand
/// ([`ResolutionWriteExt::clear_resolutions`]).
pub fn resolve_references<P, R, V, I, Path, S>(
  query: &QueryClient<P>,
  writer: &mut PartitionWriteContextRef<'_, P>,
  resolver: &R,
) -> Vec<ResolutionFailure<V, I, Path, S>>
where
  P: Partitions,
  R: SymboliqueResolver<V, I, Path, S>,
  V: Value<I> + 'static,
  I: Ident + Hash,
  Path: SymbolPath + 'static,
  S: Visibility,
  P::Stores: HasPartition<FileSymbols<V, I, Path, S>>
    + HasPartition<Symbols<V, I, Path, S>>
    + HasPartition<SymbolResolution<V, I, Path, S>>,
  SymbolResolution<V, I, Path, S>: laburnum::database::partitions::SortKeyOf<P>
    + laburnum::database::Partition<
      SortKey = Path,
      IndexEntry = crate::partitions::records::ResolutionEntry<V, I, Path, S>,
    >,
{
  let ctx = QueryResolveCtx::new(query);
  let mut failures = Vec::new();

  let entries =
    query.index_entries::<FileSymbols<V, I, Path, S>>();
  for (_key, entry) in &entries {
    let Some(record) =
      query.get::<Symbols<V, I, Path, S>>(entry.symbol.content_hash())
    else {
      continue;
    };
    // Only the final, nameable reference participates in resolution;
    // intermediate path segments (nameable == false) are skipped.
    let Some(Symbol::Reference {
      path,
      target_path,
      nameable: true,
      ..
    }) = record.record()
    else {
      continue;
    };

    match resolver.resolve(&ctx, target_path, path, entry.clone()) {
      | Resolution::Resolved(target) => {
        writer.write_resolution::<V, I, Path, S>(
          path.clone(),
          target_path.clone(),
          target.symbol,
        );
      },
      | Resolution::Deferred => {},
      | resolution => {
        failures.push(ResolutionFailure {
          reference_path: path.clone(),
          target_path: target_path.clone(),
          span: entry.span,
          resolution,
        });
      },
    }
  }

  failures
}