Multi-File Analysis

A compilation project usually consists of multiple compilation units that are semantically connected to each other.

For example, a Java file may declare a class with signatures that reference classes declared in other files within the same Java package.

To establish semantic relationships between these compilation units, you can define a special analyzer-wide feature object.

From the Shared Semantics example:

#[derive(Node)]

// Defines a semantic feature that is shared across all documents in the Analyzer.
#[semantics(CommonSemantics)]

pub enum SharedSemanticsNode {
    // ...
}

#[derive(Feature)]
#[node(SharedSemanticsNode)]
pub struct CommonSemantics {
    pub modules: Slot<SharedSemanticsNode, HashMap<String, Id>>,
}

Common Semantics

The common semantics feature is a typical feature object, except that it is not bound to any specific node within a compilation unit and is instantiated during the creation of the Analyzer.

This feature is not tied to any syntax tree scope. Therefore, its members will not be directly invalidated during the editing of the Analyzer's documents.

However, the members of this feature are part of the semantic graph and are subject to the normal rules of the semantic graph, such as the prohibition of cycles between computable functions.

Common semantic features typically include:

  • Analyzer-wide reducing attributes, such as an attribute that collects all syntax and semantic issues detected across all managed documents.
  • External configuration metadata specified via the system of Slots. For instance, a map between file names and their document IDs within the Analyzer (as in the example above).

You can access common semantics both inside and outside of computable functions. Inside a computable function, you can access common semantics using the AttrContext::common method. To access the semantics outside, you would use the AbstractTask::common method.

#[derive(Clone, PartialEq, Eq)]
pub enum KeyResolution {
    Unresolved,
    Recusrive,
    Number(usize),
}

impl Computable for KeyResolution {
    type Node = SharedSemanticsNode;

    fn compute<H: TaskHandle, S: SyncBuildHasher>(
        context: &mut AttrContext<Self::Node, H, S>,
    ) -> AnalysisResult<Self> {
        // ...

        // Reading the common semantics inside the computable function.
        let modules = context.common().modules.read(context).unwrap_abnormal()?;
        
        // ...
    }
}

let handle = TriggerHandle::new();
let mut task = analyzer.mutate(&handle, 1).unwrap();

let doc_id = task.add_mutable_doc("x = 10; y = module_2::b; z = module_2::c;");
doc_id.set_name("module_1");

// Modifying the Slot value of the common semantics outside.
task.common()
    .modules
    .mutate(&task, |modules| {
        let _ = modules.insert(String::from("module_1"), doc_id);

        true
    })
    .unwrap();

Slots

The primary purpose of a Slot is to provide a convenient mechanism for injecting configuration metadata external to the Analyzer into the semantic graph. For instance, mapping between file system names and the Analyzer's document IDs can be injected through a common semantics Slot.

Slot is a special feature of the semantic graph that is quite similar to attributes, except that a Slot does not have an associated computable function. Instead, Slots have associated values of a specified type (the second generic argument of the Slot<Node, ValueType> signature).

You can snapshot the current Slot value outside of computable functions, and you can read Slot values within the computable functions of attributes, thereby subscribing those attributes to changes in the Slot, much like with normal attributes.

By default, Slot values are set to the Default of the value type. You can modify the content of the Slot value using the Slot::mutate method with a mutable (or exclusive) task.

task.common()
    .modules
    // The `task` is a MutationTask or an ExclusiveTask.
    //
    // The provided callback accepts a mutable reference to the current
    // value of the Slot, and returns a boolean flag indicating whether the
    // value has changed.
    .mutate(&task, |modules| {
        let _ = modules.insert(String::from("module_1"), doc_id);

        // Indicates that the `modules` content has been changed.
        true
    })
    .unwrap();