symbolique 0.1.0

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 writer extension trait.
//!
//! This module provides [`ResolutionWriteExt`], for writing resolution mappings
//! to the `SymbolResolution` partition. Do not call `writer.index_entry()`
//! directly for symbolique partitions.
//!
//! # Architecture (ADR0003)
//!
//! Unlike other stages, resolution does **not** create new symbol shapes.
//! It links existing references (in FileSymbols/LinkedSymbols/MergedSymbols)
//! to their resolved target definitions (also in those partitions).
//!
//! ```text
//! writer.write_resolution()
//!//!     └─► writer.index_entry::<SymbolResolution>(
//!             reference_path,
//!             ResolutionEntry { target_path, target: existing_handle }
//!         )
//! ```
//!
//! # Usage
//!
//! Import the trait and call methods on `PartitionWriteContextRef`:
//!
//! ```ignore
//! use symbolique::ResolutionWriteExt;
//!
//! fn resolve_symbols<P>(
//!     writer: &mut PartitionWriteContextRef<'_, P>,
//!     target_handle: RecordHandle<Symbols<MyValue, MyIdent, String>>,
//! )
//! where
//!     P: Partitions,
//!     P::Stores: HasPartition<SymbolResolution<MyValue, MyIdent, String>>,
//! {
//!     writer.write_resolution::<MyValue, MyIdent, String>(
//!         "file|ref|my_func".to_string(),  // reference path
//!         "file|fn|my_func".to_string(),   // target definition path
//!         target_handle,                    // handle to target in Symbols
//!     );
//! }
//! ```
//!
//! # Key Difference from Other Stages
//!
//! - Does **not** call `writer.store::<Symbols>()`
//! - Takes an existing `RecordHandle<Symbols>` as input
//! - Only creates `ResolutionEntry` index entries

use {
  crate::{
    core::{Ident, SymbolPath, SymbolSortKey, Value},
    partitions::{
      records::ResolutionEntry, resolution::SymbolResolution, symbols::Symbols,
    },
  },
  laburnum::database::{
    HasPartition, PartitionKey, PartitionWriteContextRef, RecordHandle,
    storage::Partitions,
  },
};

/// Extension trait for writing resolution mappings to partitions.
///
/// Provides a method for recording that a reference resolves to a specific
/// target definition. Unlike other stages, resolution does not create new
/// symbol shapes - it links existing shapes.
///
/// # Type Parameters (on methods)
///
/// - `V`: Value type for literals
/// - `I`: Identifier type for names
/// - `Path`: Symbol path type (used for index keys and symbol paths)
///
/// # Example
///
/// ```ignore
/// use symbolique::ResolutionWriteExt;
///
/// fn resolve_reference(
///     writer: &mut PartitionWriteContextRef<'_, MyPartitions>,
///     reference_path: String,
///     target_path: String,
///     target_handle: RecordHandle<Symbols<MyValue, MyIdent, String>>,
/// ) {
///     writer.write_resolution::<MyValue, MyIdent, String>(
///         reference_path,
///         target_path,
///         target_handle,
///     );
/// }
/// ```
pub trait ResolutionWriteExt<P: Partitions> {
  /// Clear all resolutions for a prefix before re-resolving.
  ///
  /// Call this with the file/workspace prefix before writing new resolutions
  /// to ensure old data is removed.
  fn clear_resolutions<V, I, Path>(
    &mut self,
    prefix: &Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    P::Stores: HasPartition<SymbolResolution<V, I, Path>>;

  /// Write a resolution mapping.
  ///
  /// Records that a reference at `reference_path` resolves to the definition
  /// at `target_path`, with `target_handle` pointing to the target's shape
  /// in the `Symbols` partition.
  ///
  /// # Arguments
  ///
  /// - `reference_path`: Path of the reference being resolved (used as index key)
  /// - `target_path`: Path of the target definition
  /// - `target_handle`: Handle to the target's shape in the `Symbols` partition
  fn write_resolution<V, I, Path>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    P::Stores: HasPartition<SymbolResolution<V, I, Path>>;
}

impl<P: Partitions> ResolutionWriteExt<P> for PartitionWriteContextRef<'_, P> {
  fn clear_resolutions<V, I, Path>(
    &mut self,
    prefix: &Path,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    P::Stores: HasPartition<SymbolResolution<V, I, Path>>,
  {
    self.clear_prefix(
      SymbolResolution::<V, I, Path>::KEY,
      SymbolSortKey::from_path(prefix),
    );
  }

  fn write_resolution<V, I, Path>(
    &mut self,
    reference_path: Path,
    target_path: Path,
    target_handle: RecordHandle<Symbols<V, I, Path>>,
  ) where
    V: Value<I>,
    I: Ident,
    Path: SymbolPath,
    P::Stores: HasPartition<SymbolResolution<V, I, Path>>,
  {
    let entry = ResolutionEntry::new(target_path, target_handle);
    self.index_entry::<SymbolResolution<V, I, Path>>(
      SymbolSortKey::from_path(&reference_path),
      entry,
    );
  }
}

#[cfg(test)]
mod tests {
  use crate::{
    Symbol, SymbolVisibility,
    partitions::{
      SymbolResolution, Symbols,
      test_support::{TestPartitions, TestStores},
    },
    test_helpers::{DV, SI, TP, test_span, test_span_cache},
  };
  use laburnum::database::{
    HasPartition, PartitionWriteContextRef,
    chunk::RecordWriter,
  };
  use super::ResolutionWriteExt;
  use crate::SymboliqueWriteExt;

  fn make_writer() -> RecordWriter<TestPartitions> {
    RecordWriter::<TestPartitions>::new(laburnum::Ident::new("test"))
  }

  #[test]
  fn write_resolution_round_trip() {
    let mut writer = make_writer();
    let mut cache = test_span_cache();
    let span = test_span(&mut cache, 0);

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);

      // First, write a definition to get a target handle
      let target_handle = ctx.write_symbol_definition::<DV, SI, TP>(
        "file|fn|target".to_string(),
        span,
        SI::new("target"),
        None,
        SymbolVisibility::Public,
      );

      // Write a resolution mapping
      ctx.write_resolution::<DV, SI, TP>(
        "file|ref|call_target".to_string(),
        "file|fn|target".to_string(),
        target_handle,
      );
    }

    let chunk = writer.build();
    let stores = chunk.storage();

    let res_store =
      <TestStores as HasPartition<SymbolResolution<DV, SI, TP>>>::store(
        stores,
      );
    let entry = res_store.index_get("file|ref|call_target");
    assert!(entry.is_some());

    let entry = entry.as_ref();
    assert_eq!(
      entry.map(|e| e.target_path.as_str()),
      Some("file|fn|target"),
    );

    // Verify the target handle points to the correct symbol
    let sym_store =
      <TestStores as HasPartition<Symbols<DV, SI, TP>>>::store(stores);
    let record = entry
      .and_then(|e| sym_store.get_by_handle(&e.target))
      .as_ref()
      .and_then(|r| r.record())
      .cloned();
    match record {
      Some(Symbol::Definition { name, .. }) => {
        assert_eq!(name.as_str(), "target");
      }
      _ => panic!("expected Definition"),
    }
  }

  #[test]
  fn clear_resolutions_records_prefix() {
    let mut writer = make_writer();

    {
      let mut ctx = PartitionWriteContextRef::new(&mut writer);
      ctx.clear_resolutions::<DV, SI, TP>(
        &"file|".to_string(),
      );
    }

    let prefixes = writer.clear_prefixes();
    assert_eq!(prefixes.len(), 1);
    assert_eq!(prefixes[0].1, "file|");
  }
}