Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions architecture/07-implementation-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ diagnostics.
- compiler-backed LSP diagnostics, related labels, quick fixes, document
symbols, direct-call parameter inlay hints, and dependency-free same-Bubble
snapshots;
- compiler-resolved definition navigation for namespace-scope functions and
direct calls within those snapshots;
- validated canonical binary/library scaffolding through `pop new` and
`pop initialize`.

Expand Down
17 changes: 12 additions & 5 deletions architecture/08.1-closed-design-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,13 @@ language server consumes a version-coupled compiler tooling projection. ADR
namespace-scope document symbols for immutable open-Module snapshots. ADR 0090
adds structured related diagnostics, current compiler quick fixes, direct-call
parameter hints, and conservative same-Bubble analysis for dependency-free
Packages. The projection carries typed identities and source spans, never
compiler arenas, HIR/MIR values, CLI text, or string-based resolution.
Completion, signature help, cross-Bubble navigation, references, rename,
formatting, semantic tokens, incremental edits, and complete
Workspace/dependency analysis remain separately reviewed work.
Packages. ADR 0092 adds definition navigation for namespace-scope functions and
resolved direct calls inside those exact snapshots. The projection carries
typed identities and source spans, never compiler arenas, HIR/MIR values, CLI
text, or string-based resolution. Completion, signature help, local/member or
cross-Bubble navigation, references, rename, formatting, semantic tokens,
incremental edits, and complete Workspace/dependency analysis remain
separately reviewed work.

### 1. Compiler implementation language and repository layout

Expand Down Expand Up @@ -668,6 +670,11 @@ mismatch. Target objects stay opaque and documentation stays separate. See ADR
boundary first, then enables conventional same-Bubble analysis only when that
Bubble has no unresolved dependencies. Nested Packages never merge. See
[ADR 0090](./decisions/0090-rich-private-editor-analysis.md).
- Definition navigation joins compiler-projected occurrences and declarations
only by `SymbolIdentity`. It is initially limited to namespace-scope
functions and resolved direct calls inside one dependency-free Bubble
snapshot. Paths only present locations. See
[ADR 0092](./decisions/0092-private-semantic-definition-navigation.md).
- `pop new` and `pop initialize` create only validated canonical binary or
library Package scaffolds. They never rewrite identities, overwrite protected
entries, initialize version control, or download dependencies. See
Expand Down
13 changes: 10 additions & 3 deletions architecture/21-cli-tooling-and-code-units.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,11 +567,18 @@ the nearest ancestor Package manifest selects conventional same-Bubble Modules
when that Bubble has no unresolved dependency edge. Nested Packages remain
distinct; an outer Workspace or editor folder never merges their visibility.

ADR 0092 adds `textDocument/definition` for namespace-scope function
declarations and statically resolved direct calls in that same immutable
Bubble snapshot. The compiler projects exact occurrence spans and
`SymbolIdentity`; the adapter joins by that identity and uses paths only to
present the destination URI. Unsupported, unresolved, indirect, or
dependency-owned uses return no destination.

The adapter is a private executable protocol boundary, not the public
`Pop.Lsp` API and not a re-export of `Pop.Rpc`. Completion, signature help,
cross-Bubble navigation, references, rename, formatting, semantic tokens,
incremental text edits, complete Workspace/dependency analysis, and public
transport types require their separately reviewed schemas.
local/member or cross-Bubble navigation, references, rename, formatting,
semantic tokens, incremental text edits, complete Workspace/dependency
analysis, and public transport types require their separately reviewed schemas.
Editor extensions launch the server directly and consume structured LSP data;
they may invoke `pop` commands for explicit user actions but never scrape CLI
human output to synthesize language-server results.
Expand Down
111 changes: 111 additions & 0 deletions architecture/decisions/0092-private-semantic-definition-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# ADR 0092: Private Semantic Definition Navigation

- Status: accepted
- Date: 2026-07-18
- Supersedes: none
- Amends: ADR 0089, ADR 0090

## Context

ADRs 0089 and 0090 authorize a version-coupled compiler tooling projection and
conservative same-Bubble Package snapshots. The private language server can
therefore prove which declaration a direct call selects, but it discards that
identity after producing parameter inlay hints. Editors cannot navigate even
between two Modules that the same compiler query has already resolved.

Definition navigation cannot be recovered safely from spelling, namespaces,
filenames, numeric IDs, or CLI output. Complete Workspace dependency loading,
public source and syntax schemas, local binding indexes, references, and rename
remain separate work.

## Decision

The private compiler tooling projection may expose resolved definition
occurrences. Each occurrence contains:

- the exact source selection span;
- the resolved `SymbolIdentity`; and
- no syntax, resolver, HIR, or MIR value.

The projection initially covers namespace-scope function declarations and
statically resolved direct function calls inside one analyzed Bubble. A
declaration name is an occurrence of its own identity. Unresolved, indirect,
referenced-dependency, method, member, local, and parameter uses do not produce
an occurrence in this slice.

The private language server implements LSP 3.17
`textDocument/definition`. It joins an occurrence to a declaration by
`SymbolIdentity`, returns the declaration selection range, and returns `null`
when either side is absent. The request reads the current immutable document
snapshot, checks cancellation, and uses UTF-16 positions. A definition in
another source-owned Module of the same selected Bubble may return that
Module's file URI.

One Bubble analysis snapshot owns a deterministic map from session `FileId`
values to source URIs and text. An already open Module keeps its session
`FileId`; a closed Module receives a deterministic snapshot-local ID. File
paths and URIs select and present source inputs only. They never establish
symbol identity, merge visibility, or replace the Item → Module → Bubble →
Package → Workspace hierarchy.

The existing ADR 0090 restrictions remain active. Package snapshots are used
only for conventionally discovered Bubbles without unresolved dependency
edges. Dependency references, sibling Bubbles, nested Packages, and editor
workspace folders are never guessed or merged. Complete Workspace snapshots
must later reuse the Package resolver and locked dependency graph rather than
extend this filesystem bootstrap heuristically.

References, rename, completion, signature help, local/member navigation,
cross-Bubble navigation, public `Pop.Syntax`/`Pop.Lsp` schemas, and incremental
range edits remain outside this decision.

## Consequences

- Same-Module and same-Bubble direct calls gain exact definition navigation.
- Navigation reuses compiler resolution instead of building a competing editor
index.
- The snapshot retains enough source identity to present cross-Module
locations while semantic identity remains path-independent.
- Unsupported or incomplete analysis fails closed with `null`.

## Alternatives considered

### Search names in open text

Rejected because spelling cannot prove overload selection, visibility,
shadowing, or Bubble identity.

### Navigate by namespace and filename

Rejected because neither value is semantic identity and Modules do not derive
identity from directory layout.

### Expose resolver or HIR nodes to the server

Rejected because compiler-private arenas and IR are unstable ownership
boundaries and are not public tooling schemas.

### Implement references and rename together

Rejected because multi-file edits, dependency indexes, locals, members, stale
snapshot handling, and atomic verification require a broader contract.

## Required conformance tests

- a declaration name and a same-Module direct call navigate to the exact
declaration selection;
- a call in one Module navigates to a declaration in a sibling Module of the
same dependency-free Bubble;
- UTF-16 request and result positions remain exact around non-BMP text;
- unresolved, indirect, dependency-bearing, stale, and closed snapshots do not
fabricate a destination;
- two Packages or Bubbles with the same namespace never merge;
- the compiler projection joins occurrences and declarations only by
`SymbolIdentity`; and
- advertised LSP capabilities exactly match the implemented request.

## Documents/components affected

CLI/tooling architecture, implementation roadmap, closed design decisions,
compiler driver tooling projections, private language-server snapshots and
transport, official editor extensions, and architecture conformance tests.
25 changes: 25 additions & 0 deletions crates/compiler/driver/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub struct FrontEndResult {
pub(crate) reference_metadata: Result<ReferenceMetadata, ReferenceMetadataError>,
pub(crate) checked_documentation: Vec<CheckedDocumentation>,
pub(crate) tooling_declarations: Vec<ToolingDeclaration>,
pub(crate) tooling_definition_occurrences: Vec<ToolingDefinitionOccurrence>,
pub(crate) tooling_inlay_hints: Vec<ToolingInlayHint>,
}

Expand Down Expand Up @@ -193,6 +194,25 @@ pub enum ToolingDeclarationKind {
Enum,
}

/// Compiler-resolved source occurrence for private definition navigation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolingDefinitionOccurrence {
pub(crate) identity: SymbolIdentity,
pub(crate) selection_span: SourceSpan,
}

impl ToolingDefinitionOccurrence {
#[must_use]
pub const fn identity(&self) -> SymbolIdentity {
self.identity
}

#[must_use]
pub const fn selection_span(&self) -> SourceSpan {
self.selection_span
}
}

/// Compiler-proven parameter name attached to one direct-call argument.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolingInlayHint {
Expand Down Expand Up @@ -741,6 +761,11 @@ impl FrontEndResult {
&self.tooling_declarations
}

#[must_use]
pub fn tooling_definition_occurrences(&self) -> &[ToolingDefinitionOccurrence] {
&self.tooling_definition_occurrences
}

#[must_use]
pub fn tooling_inlay_hints(&self) -> &[ToolingInlayHint] {
&self.tooling_inlay_hints
Expand Down
38 changes: 38 additions & 0 deletions crates/compiler/driver/src/front_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ pub fn analyze_bubble(input: FrontEndBubbleInput) -> FrontEndResult {
emit_reference_metadata(hir, database.index(), resolver.arena())
});
let tooling_inlay_hints = hir.as_ref().map_or_else(Vec::new, tooling_inlay_hints);
let tooling_definition_occurrences =
tooling_definition_occurrences(input.bubble, hir.as_ref(), &tooling_declarations);
FrontEndResult {
hir,
hir_bubble_error,
Expand All @@ -309,10 +311,46 @@ pub fn analyze_bubble(input: FrontEndBubbleInput) -> FrontEndResult {
reference_metadata,
checked_documentation,
tooling_declarations,
tooling_definition_occurrences,
tooling_inlay_hints,
}
}

fn tooling_definition_occurrences(
bubble: BubbleId,
hir: Option<&HirBubble>,
declarations: &[ToolingDeclaration],
) -> Vec<ToolingDefinitionOccurrence> {
let mut occurrences = declarations
.iter()
.filter(|declaration| declaration.kind() == ToolingDeclarationKind::Function)
.map(|declaration| ToolingDefinitionOccurrence {
identity: declaration.identity(),
selection_span: declaration.selection_span(),
})
.collect::<Vec<_>>();
if let Some(hir) = hir {
occurrences.extend(
hir.functions()

@cubic-dev-ai cubic-dev-ai Bot Jul 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Definition navigation is absent for every direct call inside an @CompileTime function because this projection only scans runtime HIR, which excludes those functions. Including the already-resolved compile-time typed bodies in the projection would cover these namespace-scope calls consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/compiler/driver/src/front_end.rs, line 334:

<comment>Definition navigation is absent for every direct call inside an `@CompileTime` function because this projection only scans runtime HIR, which excludes those functions. Including the already-resolved compile-time typed bodies in the projection would cover these namespace-scope calls consistently.</comment>

<file context>
@@ -309,10 +311,46 @@ pub fn analyze_bubble(input: FrontEndBubbleInput) -> FrontEndResult {
+        .collect::<Vec<_>>();
+    if let Some(hir) = hir {
+        occurrences.extend(
+            hir.functions()
+                .iter()
+                .chain(hir.methods().iter().map(pop_hir::HirMethod::function))
</file context>
Fix with cubic

.iter()
.chain(hir.methods().iter().map(pop_hir::HirMethod::function))
.flat_map(pop_hir::hir_source_calls)
.map(|call| ToolingDefinitionOccurrence {
identity: SymbolIdentity::new(bubble, call.target()),
selection_span: call.callee_span(),
}),
);
}
occurrences.sort_by_key(|occurrence| {
(
occurrence.selection_span.file(),
occurrence.selection_span.range().start(),
occurrence.identity,
)
});
occurrences
}

fn tooling_inlay_hints(hir: &HirBubble) -> Vec<ToolingInlayHint> {
let owners = hir
.functions()
Expand Down
37 changes: 37 additions & 0 deletions crates/compiler/driver/tests/front_end_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@ use pop_hir::{HirCallDispatch, HirDeclarationKind, HirExpressionKind, HirStateme
use pop_mir::{MirDeclarationKind, MirVerificationError, lower_hir_bubble};
use pop_source::SourceFile;

#[test]
fn tooling_definition_occurrences_preserve_overload_identity_and_callee_span() {
let text = "namespace Main\nfunction choose(value: Int): Int\n return value\nend\nfunction choose(value: String): String\n return value\nend\nfunction run(): String\n return choose(\"selected\")\nend\n";
let source = SourceFile::new(FileId::from_raw(0), "src/navigation.pop", text).expect("source");
let result = analyze_bubble(FrontEndBubbleInput::new(
BubbleId::from_raw(7),
NamespaceId::from_raw(0),
Vec::new(),
vec![FrontEndModule::new(ModuleId::from_raw(0), source)],
));
assert!(
result.diagnostics().is_empty(),
"{}",
result.diagnostic_snapshot()
);

let call_start = text.rfind("choose").expect("call");
let call = result
.tooling_definition_occurrences()
.iter()
.find(|occurrence| occurrence.selection_span().range().start().to_usize() == call_start)
.expect("resolved call occurrence");
assert_eq!(
call.selection_span().range().end().to_usize(),
call_start + "choose".len(),
"the occurrence covers the callee only"
);
let target = result
.tooling_declarations()
.iter()
.find(|declaration| declaration.identity() == call.identity())
.expect("selected declaration identity");
let signature = &text[target.signature_span().range().start().to_usize()
..target.signature_span().range().end().to_usize()];
assert_eq!(signature, "function choose(value: String): String");
}

#[test]
fn explicit_generic_functions_records_and_unions_reach_concrete_mir() {
let source = SourceFile::new(
Expand Down
Loading
Loading