Skip to main content

logicaffeine_lsp/
index.rs

1use std::collections::HashMap;
2use logicaffeine_base::Interner;
3use logicaffeine_language::{
4    analysis::TypeRegistry,
5    token::{Token, TokenType, BlockType, Span},
6};
7use crate::pipeline::OwnedStmt;
8
9/// Scope context for a definition — which block contains it and at what depth.
10#[derive(Debug, Clone, Default)]
11pub struct ScopeInfo {
12    /// Index into `block_spans` for the containing block, if any.
13    pub block_idx: Option<usize>,
14    /// Nesting depth (0 = top-level, 1 = inside a block, etc.)
15    pub depth: u32,
16}
17
18/// A definition in the document (variable, function, struct, enum, field, etc.)
19#[derive(Debug, Clone)]
20pub struct Definition {
21    pub name: String,
22    pub kind: DefinitionKind,
23    pub span: Span,
24    pub detail: Option<String>,
25    pub scope: ScopeInfo,
26    /// `Some(false)` = an immutable `Let` (readonly); only Variables carry this.
27    pub mutable: Option<bool>,
28    /// Literate documentation: the `## Note` block directly above this
29    /// definition's `##` header (functions, types, theorems).
30    pub doc: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum DefinitionKind {
35    Variable,
36    Function,
37    Struct,
38    Enum,
39    Field,
40    Parameter,
41    Block,
42    Variant,
43    Theorem,
44}
45
46/// A reference to a definition.
47#[derive(Debug, Clone)]
48pub struct Reference {
49    pub name: String,
50    pub span: Span,
51    pub definition_idx: Option<usize>,
52}
53
54/// One function call site, for the call hierarchy.
55#[derive(Debug, Clone)]
56pub struct CallSite {
57    /// Definition index of the ENCLOSING function — `None` for calls from
58    /// `## Main` or other non-function blocks (no hierarchy item to hang
59    /// them on).
60    pub caller: Option<usize>,
61    /// Definition index of the called function.
62    pub callee: usize,
63    pub span: Span,
64}
65
66/// The symbol index for a single document.
67#[derive(Debug, Clone, Default)]
68pub struct SymbolIndex {
69    pub definitions: Vec<Definition>,
70    pub references: Vec<Reference>,
71    pub name_to_defs: HashMap<String, Vec<usize>>,
72    /// Maps block header names to their spans.
73    pub block_spans: Vec<(String, BlockType, Span)>,
74    /// Statement spans inferred from keyword..period token ranges.
75    pub statement_spans: Vec<(String, Span)>,
76    /// Function call sites (`f(…)` and `Call f …`), caller-resolved.
77    pub call_sites: Vec<CallSite>,
78}
79
80impl SymbolIndex {
81    /// Build the symbol index from parsed statements, tokens, the type
82    /// registry, and the source text (literate docs read the prose).
83    pub fn build(
84        stmts: &[OwnedStmt],
85        tokens: &[Token],
86        type_registry: &TypeRegistry,
87        interner: &Interner,
88        source: &str,
89    ) -> Self {
90        let mut index = SymbolIndex::default();
91
92        // Phase 1: Extract definitions from parsed statements
93        index.index_statements(stmts, tokens, interner);
94
95        // Phase 2: Extract definitions from TypeRegistry (struct/enum definitions)
96        index.index_type_registry(type_registry, tokens, interner);
97
98        // Phase 3: Extract block headers and statement spans from tokens
99        index.index_tokens(tokens, interner);
100
101        // Phase 3.5: Attach literate documentation (`## Note` above a header)
102        index.index_docs(source);
103
104        // Phase 4: Index identifier references (scope-aware)
105        index.index_references(tokens, interner);
106
107        // Phase 5: Compute scope info for each definition
108        index.compute_scopes();
109
110        // Phase 6: Function call sites for the call hierarchy
111        index.index_call_sites(tokens, interner);
112
113        index
114    }
115
116    /// Attach each function/type/theorem definition's literate documentation:
117    /// the `## Note` block sitting directly above its `##` header line.
118    fn index_docs(&mut self, source: &str) {
119        for def in &mut self.definitions {
120            if !matches!(
121                def.kind,
122                DefinitionKind::Function
123                    | DefinitionKind::Struct
124                    | DefinitionKind::Enum
125                    | DefinitionKind::Theorem
126            ) {
127                continue;
128            }
129            let anchor = def.span.start.min(source.len());
130            let line_start = source[..anchor].rfind('\n').map(|i| i + 1).unwrap_or(0);
131            let is_header_line = source[line_start..]
132                .lines()
133                .next()
134                .is_some_and(|l| l.trim_start().starts_with("## "));
135            if is_header_line {
136                def.doc =
137                    logicaffeine_language::teach::doc_for_header_at(source, line_start);
138            }
139        }
140    }
141
142    /// The binding token of the NEXT unconsumed `Let <name>` /
143    /// `Let mutable <name>` in the stream — each Let statement claims its own
144    /// binding site exactly once.
145    fn claim_let_binding(
146        tokens: &[Token],
147        name: &str,
148        interner: &Interner,
149        taken: &mut std::collections::HashSet<usize>,
150    ) -> Option<Span> {
151        for (i, token) in tokens.iter().enumerate() {
152            if !matches!(token.kind, TokenType::Let) || taken.contains(&i) {
153                continue;
154            }
155            // The binding name sits within the next couple of tokens
156            // (`Let x`, `Let mutable x`).
157            for candidate in tokens.iter().skip(i + 1).take(3) {
158                if matches!(candidate.kind, TokenType::Be | TokenType::Colon) {
159                    break;
160                }
161                if resolve_token_name(candidate, interner).map(|n| n == name).unwrap_or(false) {
162                    taken.insert(i);
163                    return Some(candidate.span);
164                }
165            }
166        }
167        None
168    }
169
170    fn add_definition(&mut self, def: Definition) -> usize {
171        let idx = self.definitions.len();
172        self.name_to_defs
173            .entry(def.name.clone())
174            .or_default()
175            .push(idx);
176        self.definitions.push(def);
177        idx
178    }
179
180    fn index_statements(&mut self, stmts: &[OwnedStmt], tokens: &[Token], interner: &Interner) {
181        let mut taken_lets: std::collections::HashSet<usize> = std::collections::HashSet::new();
182        for stmt in stmts {
183            match stmt {
184                OwnedStmt::FunctionDef { name, params, return_type } => {
185                    let span = find_token_span_for_name(tokens, name, interner)
186                        .unwrap_or(Span::default());
187                    let detail = {
188                        let param_str: Vec<String> = params
189                            .iter()
190                            .map(|(n, t)| format!("{}: {}", n, t))
191                            .collect();
192                        let ret = return_type.as_deref().unwrap_or("Unit");
193                        Some(format!("To {}({}) -> {}", name, param_str.join(", "), ret))
194                    };
195                    self.add_definition(Definition {
196                        name: name.clone(),
197                        kind: DefinitionKind::Function,
198                        span,
199                        detail,
200                        scope: ScopeInfo::default(),
201                        mutable: None,
202                        doc: None,
203                    });
204
205                    // Add parameters as definitions, each with its own span
206                    let mut search_after = span.end;
207                    for (param_name, param_type) in params {
208                        let param_span = find_token_span_for_name_after(
209                            tokens, param_name, interner, search_after,
210                        ).unwrap_or(span);
211                        if param_span != span {
212                            search_after = param_span.end;
213                        }
214                        self.add_definition(Definition {
215                            name: param_name.clone(),
216                            kind: DefinitionKind::Parameter,
217                            span: param_span,
218                            detail: Some(format!("{}: {}", param_name, param_type)),
219                            scope: ScopeInfo::default(),
220                            mutable: None,
221                            doc: None,
222                        });
223                    }
224                }
225                OwnedStmt::StructDef { name, fields } => {
226                    let span = find_token_span_for_name(tokens, name, interner)
227                        .unwrap_or(Span::default());
228                    self.add_definition(Definition {
229                        name: name.clone(),
230                        kind: DefinitionKind::Struct,
231                        span,
232                        detail: Some(format!("{} (struct)", name)),
233                        scope: ScopeInfo::default(),
234                        mutable: None,
235                        doc: None,
236                    });
237                    let mut field_search_after = span.end;
238                    for (field_name, field_type) in fields {
239                        let field_span = find_token_span_for_name_after(
240                            tokens, field_name, interner, field_search_after,
241                        ).unwrap_or(span);
242                        if field_span != span {
243                            field_search_after = field_span.end;
244                        }
245                        self.add_definition(Definition {
246                            name: field_name.clone(),
247                            kind: DefinitionKind::Field,
248                            span: field_span,
249                            detail: Some(format!("{}: {}", field_name, field_type)),
250                            scope: ScopeInfo::default(),
251                            mutable: None,
252                            doc: None,
253                        });
254                    }
255                }
256                OwnedStmt::Let { name, ty, inferred_type, mutable } => {
257                    // Each re-Let of the same name anchors on its OWN
258                    // binding token (the name right after its `Let`), never
259                    // the first occurrence — shadow warnings and renames
260                    // depend on the distinction.
261                    let span = Self::claim_let_binding(tokens, name, interner, &mut taken_lets)
262                        .or_else(|| find_token_span_for_name(tokens, name, interner))
263                        .unwrap_or(Span::default());
264                    let prefix = if *mutable { "mut " } else { "" };
265                    let detail = if let Some(explicit_ty) = ty {
266                        format!("Let {}{}: {}", prefix, name, explicit_ty)
267                    } else if let Some(inferred) = inferred_type {
268                        format!("Let {}{}: {} (inferred)", prefix, name, inferred)
269                    } else {
270                        format!("Let {}{}: auto (inferred)", prefix, name)
271                    };
272                    self.add_definition(Definition {
273                        name: name.clone(),
274                        kind: DefinitionKind::Variable,
275                        span,
276                        detail: Some(detail),
277                        scope: ScopeInfo::default(),
278                        mutable: Some(*mutable),
279                        doc: None,
280                    });
281                }
282                OwnedStmt::Theorem { name } => {
283                    let span = find_token_span_for_name(tokens, name, interner)
284                        .unwrap_or(Span::default());
285                    self.add_definition(Definition {
286                        name: name.clone(),
287                        kind: DefinitionKind::Theorem,
288                        span,
289                        detail: Some(format!("Theorem {}", name)),
290                        scope: ScopeInfo::default(),
291                        mutable: None,
292                        doc: None,
293                    });
294                }
295                OwnedStmt::Block { name, kind } => {
296                    self.add_definition(Definition {
297                        name: name.clone(),
298                        kind: DefinitionKind::Block,
299                        span: Span::default(),
300                        detail: Some(format!("{} {}", kind, name)),
301                        scope: ScopeInfo::default(),
302                        mutable: None,
303                        doc: None,
304                    });
305                }
306                OwnedStmt::Other => {}
307            }
308        }
309    }
310
311    fn index_type_registry(
312        &mut self,
313        type_registry: &TypeRegistry,
314        tokens: &[Token],
315        interner: &Interner,
316    ) {
317        for (sym, typedef) in type_registry.iter_types() {
318            let name = interner.resolve(*sym).to_string();
319            // Skip primitives — they're built-in, not user-defined
320            if matches!(typedef, logicaffeine_language::analysis::TypeDef::Primitive) {
321                continue;
322            }
323            // Skip if already indexed from statements
324            if self.name_to_defs.contains_key(&name) {
325                continue;
326            }
327
328            let span = find_token_span_for_name(tokens, &name, interner)
329                .unwrap_or(Span::default());
330
331            match typedef {
332                logicaffeine_language::analysis::TypeDef::Struct { fields, .. } => {
333                    self.add_definition(Definition {
334                        name: name.clone(),
335                        kind: DefinitionKind::Struct,
336                        span,
337                        detail: Some(format!("{} (struct)", name)),
338                        scope: ScopeInfo::default(),
339                        mutable: None,
340                        doc: None,
341                    });
342                    let mut field_search_after = span.end;
343                    for field in fields {
344                        let field_name = interner.resolve(field.name).to_string();
345                        // The field's OWN token span — reusing the struct
346                        // header's span would collide in every span-keyed
347                        // map (highlighting painted struct names as fields).
348                        let field_span = find_token_span_for_name_after(
349                            tokens, &field_name, interner, field_search_after,
350                        )
351                        .unwrap_or(span);
352                        if field_span != span {
353                            field_search_after = field_span.end;
354                        }
355                        self.add_definition(Definition {
356                            name: field_name.clone(),
357                            kind: DefinitionKind::Field,
358                            span: field_span,
359                            detail: Some(format!("{}.{}", name, field_name)),
360                            scope: ScopeInfo::default(),
361                            mutable: None,
362                            doc: None,
363                        });
364                    }
365                }
366                logicaffeine_language::analysis::TypeDef::Enum { variants, .. } => {
367                    self.add_definition(Definition {
368                        name: name.clone(),
369                        kind: DefinitionKind::Enum,
370                        span,
371                        detail: Some(format!("{} (enum)", name)),
372                        scope: ScopeInfo::default(),
373                        mutable: None,
374                        doc: None,
375                    });
376                    for variant in variants {
377                        let variant_name = interner.resolve(variant.name).to_string();
378                        let variant_span = find_token_span_for_name(tokens, &variant_name, interner)
379                            .unwrap_or(span);
380                        self.add_definition(Definition {
381                            name: variant_name.clone(),
382                            kind: DefinitionKind::Variant,
383                            span: variant_span,
384                            detail: Some(format!("{}::{}", name, variant_name)),
385                            scope: ScopeInfo::default(),
386                            mutable: None,
387                            doc: None,
388                        });
389                    }
390                }
391                _ => {}
392            }
393        }
394    }
395
396    fn index_tokens(&mut self, tokens: &[Token], interner: &Interner) {
397        let mut i = 0;
398        while i < tokens.len() {
399            match &tokens[i].kind {
400                TokenType::BlockHeader { block_type } => {
401                    // Find the extent of this block (up to next block header or EOF)
402                    let start = tokens[i].span.start;
403                    let mut end = tokens.last().map(|t| t.span.end).unwrap_or(start);
404                    for j in (i + 1)..tokens.len() {
405                        if matches!(tokens[j].kind, TokenType::BlockHeader { .. }) {
406                            end = tokens[j].span.start;
407                            break;
408                        }
409                    }
410
411                    let name = interner.resolve(tokens[i].lexeme).to_string();
412                    self.block_spans.push((
413                        name,
414                        *block_type,
415                        Span::new(start, end),
416                    ));
417                }
418                _ => {}
419            }
420            i += 1;
421        }
422
423        // Index statement spans (keyword..period pairs)
424        self.index_statement_spans(tokens, interner);
425    }
426
427    fn index_statement_spans(&mut self, tokens: &[Token], interner: &Interner) {
428        let mut i = 0;
429        while i < tokens.len() {
430            let is_stmt_keyword = matches!(
431                tokens[i].kind,
432                TokenType::Let
433                    | TokenType::Set
434                    | TokenType::If
435                    | TokenType::While
436                    | TokenType::Repeat
437                    | TokenType::Return
438                    | TokenType::Show
439                    | TokenType::Give
440                    | TokenType::Push
441                    | TokenType::Pop
442                    | TokenType::Call
443                    | TokenType::Inspect
444                    | TokenType::Check
445                    | TokenType::Assert
446                    | TokenType::Trust
447                    | TokenType::Require
448                    | TokenType::Requires
449                    | TokenType::Ensures
450                    | TokenType::Escape
451                    | TokenType::Read
452                    | TokenType::Write
453                    | TokenType::Spawn
454                    | TokenType::Send
455                    | TokenType::Await
456                    | TokenType::Sleep
457                    | TokenType::Merge
458                    | TokenType::Increase
459                    | TokenType::Decrease
460                    | TokenType::Listen
461                    | TokenType::Sync
462                    | TokenType::Mount
463                    | TokenType::Launch
464                    | TokenType::Receive
465                    | TokenType::Stop
466            );
467            if is_stmt_keyword {
468                let start = tokens[i].span.start;
469                let keyword = interner.resolve(tokens[i].lexeme).to_string();
470                // Scan forward for period or next statement keyword
471                let mut end = tokens[i].span.end;
472                for j in (i + 1)..tokens.len() {
473                    end = tokens[j].span.end;
474                    if matches!(tokens[j].kind, TokenType::Period | TokenType::Dedent) {
475                        break;
476                    }
477                }
478                self.statement_spans.push((keyword, Span::new(start, end)));
479            }
480            i += 1;
481        }
482    }
483
484    fn index_references(&mut self, tokens: &[Token], interner: &Interner) {
485        for token in tokens {
486            if let Some(resolved) = resolve_token_name(token, interner) {
487                // Skip block headers — they're not references
488                if matches!(token.kind, TokenType::BlockHeader { .. }) {
489                    continue;
490                }
491                let name = resolved.to_string();
492                // Prefer the definition in the nearest scope
493                let ref_block = self.block_for_offset(token.span.start);
494                let def_idx = self.nearest_def(&name, ref_block);
495                self.references.push(Reference {
496                    name,
497                    span: token.span,
498                    definition_idx: def_idx,
499                });
500            }
501        }
502    }
503
504    /// Record every `f(…)` and `Call f …` site whose name resolves to a
505    /// function definition. The definition's own header (`## To f (…)`) also
506    /// puts the name before a paren — excluded by span identity.
507    fn index_call_sites(&mut self, tokens: &[Token], interner: &Interner) {
508        for (i, token) in tokens.iter().enumerate() {
509            let Some(name) = resolve_token_name(token, interner) else { continue };
510
511            let is_call_form = matches!(
512                tokens.get(i + 1).map(|t| &t.kind),
513                Some(TokenType::LParen)
514            ) || matches!(
515                i.checked_sub(1).and_then(|p| tokens.get(p)).map(|t| &t.kind),
516                Some(TokenType::Call)
517            );
518            if !is_call_form {
519                continue;
520            }
521
522            let Some(callee) = self
523                .name_to_defs
524                .get(name)
525                .and_then(|indices| {
526                    indices
527                        .iter()
528                        .copied()
529                        .find(|&ix| self.definitions[ix].kind == DefinitionKind::Function)
530                })
531            else {
532                continue;
533            };
534            if self.definitions[callee].span == token.span {
535                continue; // the definition's own signature, not a call
536            }
537
538            self.call_sites.push(CallSite {
539                caller: self.enclosing_function(token.span.start),
540                callee,
541                span: token.span,
542            });
543        }
544    }
545
546    /// The function definition whose block contains `offset`, if any.
547    pub fn enclosing_function(&self, offset: usize) -> Option<usize> {
548        let block = self
549            .block_spans
550            .iter()
551            .filter(|(_, block_type, span)| {
552                *block_type == BlockType::Function && span.start <= offset && offset < span.end
553            })
554            .min_by_key(|(_, _, span)| span.end - span.start)?;
555        let block_span = block.2;
556        self.definitions.iter().position(|d| {
557            d.kind == DefinitionKind::Function
558                && block_span.start <= d.span.start
559                && d.span.start < block_span.end
560        })
561    }
562
563    /// Compute scope info for each definition by matching its span against block_spans.
564    fn compute_scopes(&mut self) {
565        for i in 0..self.definitions.len() {
566            let def_start = self.definitions[i].span.start;
567            if self.definitions[i].span == Span::default() {
568                continue;
569            }
570            let mut best_block: Option<usize> = None;
571            let mut best_size = usize::MAX;
572            for (bi, (_name, _bt, bspan)) in self.block_spans.iter().enumerate() {
573                if def_start >= bspan.start && def_start < bspan.end {
574                    let size = bspan.end - bspan.start;
575                    if size < best_size {
576                        best_size = size;
577                        best_block = Some(bi);
578                    }
579                }
580            }
581            self.definitions[i].scope = ScopeInfo {
582                block_idx: best_block,
583                depth: if best_block.is_some() { 1 } else { 0 },
584            };
585        }
586    }
587
588    /// Find which block an offset falls inside (smallest containing block).
589    fn block_for_offset(&self, offset: usize) -> Option<usize> {
590        let mut best: Option<usize> = None;
591        let mut best_size = usize::MAX;
592        for (bi, (_name, _bt, bspan)) in self.block_spans.iter().enumerate() {
593            if offset >= bspan.start && offset < bspan.end {
594                let size = bspan.end - bspan.start;
595                if size < best_size {
596                    best_size = size;
597                    best = Some(bi);
598                }
599            }
600        }
601        best
602    }
603
604    /// Find the best definition index for a name, preferring same-block defs.
605    fn nearest_def(&self, name: &str, ref_block: Option<usize>) -> Option<usize> {
606        let indices = self.name_to_defs.get(name)?;
607        if indices.len() == 1 {
608            return Some(indices[0]);
609        }
610        // Prefer definition in the same block
611        if let Some(block_idx) = ref_block {
612            for &idx in indices {
613                if self.definitions[idx].scope.block_idx == Some(block_idx) {
614                    return Some(idx);
615                }
616            }
617        }
618        // Fall back to first definition
619        indices.first().copied()
620    }
621
622    /// Find the definition at the given byte offset.
623    pub fn definition_at(&self, offset: usize) -> Option<&Definition> {
624        // First check if we're on a reference
625        for reference in &self.references {
626            if offset >= reference.span.start && offset < reference.span.end {
627                if let Some(idx) = reference.definition_idx {
628                    return self.definitions.get(idx);
629                }
630            }
631        }
632        // Then check if we're on a definition itself
633        for def in &self.definitions {
634            if offset >= def.span.start && offset < def.span.end {
635                return Some(def);
636            }
637        }
638        None
639    }
640
641    /// Scope-aware definition lookup. Given a byte offset for context,
642    /// returns the definition of `name` in the nearest scope.
643    pub fn definition_at_scoped(&self, name: &str, offset: usize) -> Option<&Definition> {
644        let ref_block = self.block_for_offset(offset);
645        let idx = self.nearest_def(name, ref_block)?;
646        self.definitions.get(idx)
647    }
648
649    /// Find all references to the definition with the given name.
650    pub fn references_to(&self, name: &str) -> Vec<&Reference> {
651        self.references
652            .iter()
653            .filter(|r| r.name == name)
654            .collect()
655    }
656
657    /// Find references to `name` that are in the same scope as the definition at `def_offset`.
658    pub fn references_in_scope(&self, name: &str, def_offset: usize) -> Vec<&Reference> {
659        let def_block = self.block_for_offset(def_offset);
660        self.references
661            .iter()
662            .filter(|r| {
663                if r.name != name {
664                    return false;
665                }
666                let ref_block = self.block_for_offset(r.span.start);
667                ref_block == def_block
668            })
669            .collect()
670    }
671
672    /// Find all definitions with the given name.
673    pub fn definitions_of(&self, name: &str) -> Vec<&Definition> {
674        self.name_to_defs
675            .get(name)
676            .map(|indices| {
677                indices
678                    .iter()
679                    .filter_map(|&idx| self.definitions.get(idx))
680                    .collect()
681            })
682            .unwrap_or_default()
683    }
684
685    /// Get the block name for a block index.
686    pub fn block_name(&self, block_idx: usize) -> Option<&str> {
687        self.block_spans.get(block_idx).map(|(name, _, _)| name.as_str())
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use crate::pipeline::analyze;
695
696    #[test]
697    fn let_binding_has_nondefault_span() {
698        let result = analyze("## Main\n    Let x be 5.\n");
699        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
700        let defs = result.symbol_index.definitions_of("x");
701        assert_eq!(defs.len(), 1, "Expected 1 def for 'x', got {:?}", defs);
702        assert_ne!(defs[0].span, Span::default(),
703            "Definition span should not be default after fix");
704    }
705
706    #[test]
707    fn let_binding_span_points_to_source() {
708        let source = "## Main\n    Let x be 5.\n";
709        let result = analyze(source);
710        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
711        let defs = result.symbol_index.definitions_of("x");
712        assert_eq!(defs.len(), 1);
713        let span = defs[0].span;
714        let text = &source[span.start..span.end];
715        assert_eq!(text, "x", "Span should point to 'x' in source, got '{}'", text);
716    }
717
718    #[test]
719    fn definition_at_finds_variable() {
720        let source = "## Main\n    Let x be 5.\n";
721        let result = analyze(source);
722        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
723        let defs = result.symbol_index.definitions_of("x");
724        assert!(!defs.is_empty());
725        let span = defs[0].span;
726        let def = result.symbol_index.definition_at(span.start);
727        assert!(def.is_some(), "definition_at should find 'x' at its span");
728        assert_eq!(def.unwrap().name, "x");
729    }
730
731    #[test]
732    fn definitions_of_returns_correct_kind() {
733        let result = analyze("## Main\n    Let x be 5.\n");
734        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
735        let defs = result.symbol_index.definitions_of("x");
736        assert_eq!(defs[0].kind, DefinitionKind::Variable);
737    }
738
739    #[test]
740    fn definitions_of_unknown_returns_empty() {
741        let result = analyze("## Main\n    Let x be 5.\n");
742        let defs = result.symbol_index.definitions_of("nonexistent");
743        assert!(defs.is_empty());
744    }
745
746    #[test]
747    fn references_to_finds_usages() {
748        let result = analyze("## Main\n    Let x be 5.\n    Show x.\n");
749        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
750        let refs = result.symbol_index.references_to("x");
751        assert!(refs.len() >= 1, "Expected refs to 'x', got {}", refs.len());
752    }
753
754    #[test]
755    fn reference_linked_to_definition() {
756        let result = analyze("## Main\n    Let x be 5.\n    Show x.\n");
757        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
758        let refs = result.symbol_index.references_to("x");
759        let linked: Vec<_> = refs.iter().filter(|r| r.definition_idx.is_some()).collect();
760        assert!(!linked.is_empty(), "At least one reference should link to a definition");
761    }
762
763    #[test]
764    fn let_binding_has_detail() {
765        let result = analyze("## Main\n    Let x be 5.\n");
766        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
767        let defs = result.symbol_index.definitions_of("x");
768        assert!(defs[0].detail.is_some(), "Definition should have detail");
769        assert!(defs[0].detail.as_ref().unwrap().contains("Let"),
770            "Detail should mention Let: {:?}", defs[0].detail);
771    }
772
773    #[test]
774    fn block_spans_populated() {
775        let result = analyze("## Main\n    Let x be 5.\n");
776        assert!(!result.symbol_index.block_spans.is_empty(),
777            "block_spans should have at least one entry");
778    }
779
780    #[test]
781    fn multiple_variables_indexed() {
782        let result = analyze("## Main\n    Let a be 1.\n    Let b be 2.\n    Let c be 3.\n");
783        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
784        assert_eq!(result.symbol_index.definitions_of("a").len(), 1);
785        assert_eq!(result.symbol_index.definitions_of("b").len(), 1);
786        assert_eq!(result.symbol_index.definitions_of("c").len(), 1);
787    }
788
789    #[test]
790    fn definition_at_whitespace_returns_none() {
791        let source = "## Main\n    Let x be 5.\n";
792        let result = analyze(source);
793        // Offset 0 is at '#' in "## Main" — there IS a BlockHeader token there
794        // But offset somewhere in pure whitespace between tokens should be None
795        // The space between "Let" and "x" is at byte 12
796        // Let's use an offset that's clearly in trailing whitespace
797        let past_end = source.len() + 5;
798        let def = result.symbol_index.definition_at(past_end);
799        assert!(def.is_none(), "definition_at beyond source should return None");
800    }
801
802    #[test]
803    fn references_to_exact_count() {
804        let source = "## Main\n    Let x be 5.\n    Show x.\n    Set x to x + 1.\n";
805        let result = analyze(source);
806        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
807        let refs = result.symbol_index.references_to("x");
808        // "x" appears in: "Let x" (def+ref), "Show x" (ref), "Set x" (ref), "x + 1" (ref)
809        // The exact count depends on tokenization, but there should be at least 3 references
810        assert!(refs.len() >= 3,
811            "Expected at least 3 references to 'x', got {}: {:?}",
812            refs.len(),
813            refs.iter().map(|r| (r.span.start, r.span.end)).collect::<Vec<_>>()
814        );
815    }
816
817    #[test]
818    fn definitions_of_function() {
819        let source = "## To greet (name: Text):\n    Show name.\n## Main\n    Call greet with \"Alice\".\n";
820        let result = analyze(source);
821        let defs = result.symbol_index.definitions_of("greet");
822        let func_defs: Vec<_> = defs.iter().filter(|d| d.kind == DefinitionKind::Function).collect();
823        assert!(!func_defs.is_empty(), "Expected a Function definition for 'greet', got defs: {:?}", defs);
824        assert_eq!(func_defs[0].kind, DefinitionKind::Function);
825    }
826
827    #[test]
828    fn parameter_has_own_span() {
829        let source = "## To greet (name: Text):\n    Show name.\n";
830        let result = analyze(source);
831        assert!(result.errors.is_empty(), "Errors: {:?}", result.errors);
832
833        let func_defs = result.symbol_index.definitions_of("greet");
834        let param_defs = result.symbol_index.definitions_of("name");
835
836        assert!(!func_defs.is_empty(), "Should have function def for 'greet'");
837        assert!(!param_defs.is_empty(), "Should have param def for 'name'");
838
839        let func_span = func_defs.iter()
840            .find(|d| d.kind == DefinitionKind::Function)
841            .map(|d| d.span);
842        let param_span = param_defs.iter()
843            .find(|d| d.kind == DefinitionKind::Parameter)
844            .map(|d| d.span);
845
846        if let (Some(fs), Some(ps)) = (func_span, param_span) {
847            if fs != Span::default() && ps != Span::default() {
848                assert_ne!(fs, ps,
849                    "Parameter 'name' span {:?} should differ from function 'greet' span {:?}",
850                    ps, fs);
851                let param_text = &source[ps.start..ps.end];
852                assert_eq!(param_text, "name",
853                    "Parameter span should point to 'name' in source, got '{}'", param_text);
854            }
855        }
856    }
857
858    #[test]
859    fn second_occurrence_found() {
860        let source = "## Main\n    Let x be 5.\n    Let x be 10.\n";
861        let result = analyze(source);
862        // Even if there are parse errors due to redefinition, we should get definitions
863        let defs = result.symbol_index.definitions_of("x");
864        // There should be at least one definition
865        assert!(!defs.is_empty(), "Expected at least one definition for 'x'");
866    }
867}
868
869/// Resolve the user-visible name from a token, if it carries one.
870pub fn resolve_token_name<'a>(token: &Token, interner: &'a Interner) -> Option<&'a str> {
871    match &token.kind {
872        TokenType::Identifier => Some(interner.resolve(token.lexeme)),
873        TokenType::ProperName(sym) => Some(interner.resolve(*sym)),
874        TokenType::Noun(sym) => Some(interner.resolve(*sym)),
875        TokenType::Adjective(sym) => Some(interner.resolve(*sym)),
876        TokenType::BlockHeader { .. } => Some(interner.resolve(token.lexeme)),
877        // Definitions are named by their SURFACE form ("greet"), not the
878        // lexicon's normalized lemma ("Greet") — an English word used as an
879        // identifier must resolve by what the author wrote.
880        TokenType::Verb { .. } => Some(interner.resolve(token.lexeme)),
881        // A lexically ambiguous word ("name": verb or noun) is still an
882        // identifier at the surface level.
883        TokenType::Ambiguous { .. } => Some(interner.resolve(token.lexeme)),
884        _ => None,
885    }
886}
887
888/// Find the first token span where a name appears in the token stream,
889/// optionally starting after a given byte offset.
890fn find_token_span_for_name(tokens: &[Token], name: &str, interner: &Interner) -> Option<Span> {
891    find_token_span_for_name_after(tokens, name, interner, 0)
892}
893
894/// Public version of `find_token_span_for_name` for use by other LSP modules.
895pub fn find_token_span_for_name_pub(tokens: &[Token], name: &str, interner: &Interner) -> Option<Span> {
896    find_token_span_for_name(tokens, name, interner)
897}
898
899/// The LAST occurrence of a name — the use-site approximation for
900/// use-after-move/escape diagnostics when no statement span is known (the
901/// complaint is always about a later use, never the binding itself).
902pub fn find_last_token_span_for_name(
903    tokens: &[Token],
904    name: &str,
905    interner: &Interner,
906) -> Option<Span> {
907    tokens
908        .iter()
909        .rev()
910        .find(|t| {
911            !matches!(t.kind, TokenType::BlockHeader { .. })
912                && resolve_token_name(t, interner).map(|n| n == name).unwrap_or(false)
913        })
914        .map(|t| t.span)
915}
916
917/// Find the first token span where a name appears after `after_offset`.
918fn find_token_span_for_name_after(
919    tokens: &[Token],
920    name: &str,
921    interner: &Interner,
922    after_offset: usize,
923) -> Option<Span> {
924    for token in tokens {
925        if token.span.start < after_offset {
926            continue;
927        }
928        if let Some(resolved) = resolve_token_name(token, interner) {
929            if resolved == name {
930                return Some(token.span);
931            }
932        }
933    }
934    None
935}
936
937/// Find the span of the keyword statement (Give, Zone, …) that CAUSED an
938/// error on `variable_name`. Used for diagnostic related-information.
939///
940/// The variable must sit in OBJECT position — the token immediately after
941/// the keyword — so `Give y to x` never counts as the Give that moved `x`
942/// (there, `x` is the recipient). When `before_offset` is given, the LAST
943/// matching statement before that use site wins: the most recent move is
944/// the cause, not the first one in the file.
945pub fn find_keyword_span_before_name(
946    tokens: &[Token],
947    keyword: TokenType,
948    variable_name: &str,
949    interner: &Interner,
950) -> Option<Span> {
951    find_cause_keyword_span(tokens, keyword, variable_name, interner, usize::MAX)
952}
953
954/// [`find_keyword_span_before_name`] bounded to causes before a use site.
955pub fn find_cause_keyword_span(
956    tokens: &[Token],
957    keyword: TokenType,
958    variable_name: &str,
959    interner: &Interner,
960    before_offset: usize,
961) -> Option<Span> {
962    let discriminant = std::mem::discriminant(&keyword);
963    let mut best: Option<Span> = None;
964    for (i, token) in tokens.iter().enumerate() {
965        if std::mem::discriminant(&token.kind) != discriminant {
966            continue;
967        }
968        if token.span.start >= before_offset {
969            break;
970        }
971        let object_matches = tokens
972            .get(i + 1)
973            .and_then(|t| resolve_token_name(t, interner))
974            .map(|resolved| resolved == variable_name)
975            .unwrap_or(false);
976        if object_matches {
977            best = Some(token.span);
978        }
979    }
980    best
981}