Skip to main content

lmntalc_ide/
lib.rs

1mod language;
2mod outline;
3mod reference;
4mod semantic;
5
6use std::collections::HashMap;
7
8use language::{LanguageInfo, build_language_info};
9use lmntalc_core::{
10    codegen::{Emitter, IRSet},
11    lowering::{self, TransformResult},
12    semantics::{SemanticAnalysisResult, analyze},
13    syntax::{
14        ast::{
15            Atom, Hyperlink, Link, LinkBundle, Membrane, Process, ProcessContext, ProcessList,
16            Rule, RuleContext,
17        },
18        lexing::{Lexer, LexingResult},
19        parsing::{Parser, ParsingResult},
20    },
21};
22
23pub use lmntalc_core::diagnostics::{Diagnostic, DiagnosticSeverity, DiagnosticStage, RelatedSpan};
24pub use lmntalc_core::text::{Pos, Source, Span};
25pub use outline::{OutlineKind, OutlineSymbol};
26pub use reference::ReferenceIndex;
27pub use semantic::{SemanticKind, SemanticSpan};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AnalysisDepth {
31    Semantic,
32    Lowering,
33    Ir,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct AnalysisConfig {
38    pub depth: AnalysisDepth,
39}
40
41impl Default for AnalysisConfig {
42    fn default() -> Self {
43        Self {
44            depth: AnalysisDepth::Semantic,
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SyntaxNodeKind {
51    Membrane,
52    Rule,
53    ProcessList,
54    Atom,
55    Link,
56    Hyperlink,
57    ProcessContext,
58    RuleContext,
59    LinkBundle,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct SyntaxNode {
64    pub kind: SyntaxNodeKind,
65    pub span: Span,
66    pub name: Option<String>,
67}
68
69#[derive(Debug)]
70pub struct DocumentSnapshot {
71    uri: String,
72    version: i32,
73    source: Source,
74    lexing: LexingResult,
75    parsing: Option<ParsingResult>,
76    semantics: Option<SemanticAnalysisResult>,
77    lowering: Option<TransformResult>,
78    ir: Option<IRSet>,
79    language: Option<LanguageInfo>,
80}
81
82impl DocumentSnapshot {
83    pub fn uri(&self) -> &str {
84        &self.uri
85    }
86
87    pub fn version(&self) -> i32 {
88        self.version
89    }
90
91    pub fn source(&self) -> &Source {
92        &self.source
93    }
94
95    pub fn lexing(&self) -> &LexingResult {
96        &self.lexing
97    }
98
99    pub fn parsing(&self) -> Option<&ParsingResult> {
100        self.parsing.as_ref()
101    }
102
103    pub fn semantics(&self) -> Option<&SemanticAnalysisResult> {
104        self.semantics.as_ref()
105    }
106
107    pub fn lowering(&self) -> Option<&TransformResult> {
108        self.lowering.as_ref()
109    }
110
111    pub fn ir(&self) -> Option<&IRSet> {
112        self.ir.as_ref()
113    }
114
115    pub fn diagnostics(&self) -> Vec<Diagnostic> {
116        let mut diagnostics = self.lexing.diagnostics();
117        if let Some(parsing) = &self.parsing {
118            diagnostics.extend(parsing.diagnostics());
119        }
120        if let Some(semantics) = &self.semantics {
121            diagnostics.extend(semantics.diagnostics());
122        }
123        if let Some(lowering) = &self.lowering {
124            diagnostics.extend(lowering.diagnostics());
125        }
126        diagnostics
127    }
128
129    pub fn offset_at(&self, line: u32, column: u32) -> Option<usize> {
130        offset_at(self.source(), line, column)
131    }
132
133    pub fn outline(&self) -> &[OutlineSymbol] {
134        self.language
135            .as_ref()
136            .map(LanguageInfo::outline)
137            .unwrap_or(&[])
138    }
139
140    pub fn semantic_spans(&self) -> &[SemanticSpan] {
141        self.language
142            .as_ref()
143            .map(LanguageInfo::semantic_spans)
144            .unwrap_or(&[])
145    }
146
147    pub fn references_at_offset(&self, offset: usize) -> Vec<Span> {
148        self.language
149            .as_ref()
150            .map(|language| language.reference_index().references_at_offset(offset))
151            .unwrap_or_default()
152    }
153
154    pub fn highlights_at_offset(&self, offset: usize) -> Vec<Span> {
155        self.language
156            .as_ref()
157            .map(|language| language.reference_index().highlights_at_offset(offset))
158            .unwrap_or_default()
159    }
160
161    pub fn node_at_offset(&self, offset: usize) -> Option<SyntaxNode> {
162        self.find_best_node(|span| span_contains_offset(span, offset))
163    }
164
165    pub fn node_at_span(&self, span: Span) -> Option<SyntaxNode> {
166        self.find_best_node(|candidate| candidate.contains(span))
167    }
168
169    fn find_best_node<F>(&self, predicate: F) -> Option<SyntaxNode>
170    where
171        F: Fn(Span) -> bool,
172    {
173        let parsing = self.parsing.as_ref()?;
174        let mut nodes = Vec::new();
175        collect_membrane_nodes(&parsing.root, &mut nodes);
176        nodes
177            .into_iter()
178            .filter(|node| predicate(node.span))
179            .min_by_key(|node| (node.span.len(), node_specificity(node.kind)))
180    }
181}
182
183#[derive(Debug, Default)]
184pub struct AnalysisSession {
185    config: AnalysisConfig,
186    documents: HashMap<String, DocumentSnapshot>,
187}
188
189impl AnalysisSession {
190    pub fn new() -> Self {
191        Self::default()
192    }
193
194    pub fn with_config(config: AnalysisConfig) -> Self {
195        Self {
196            config,
197            documents: HashMap::new(),
198        }
199    }
200
201    pub fn set_document(&mut self, uri: impl Into<String>, version: i32, text: impl Into<String>) {
202        let uri = uri.into();
203        let snapshot = build_snapshot(self.config, uri.clone(), version, text.into());
204        self.documents.insert(uri, snapshot);
205    }
206
207    pub fn remove_document(&mut self, uri: &str) -> Option<DocumentSnapshot> {
208        self.documents.remove(uri)
209    }
210
211    pub fn snapshot(&self, uri: &str) -> Option<&DocumentSnapshot> {
212        self.documents.get(uri)
213    }
214
215    pub fn diagnostics(&self, uri: &str) -> Vec<Diagnostic> {
216        self.snapshot(uri)
217            .map(DocumentSnapshot::diagnostics)
218            .unwrap_or_default()
219    }
220}
221
222fn build_snapshot(
223    config: AnalysisConfig,
224    uri: String,
225    version: i32,
226    text: String,
227) -> DocumentSnapshot {
228    let source = Source::new(uri.clone(), document_name(&uri), text);
229    let lexing = Lexer::new(&source).lex();
230
231    let parsing = if lexing.errors.is_empty() {
232        Some(Parser::new().parse(lexing.tokens.clone()))
233    } else {
234        None
235    };
236
237    let language = parsing
238        .as_ref()
239        .map(|parsing| build_language_info(&parsing.root));
240
241    let semantics = parsing
242        .as_ref()
243        .filter(|parsing| parsing.parsing_errors.is_empty())
244        .map(|parsing| analyze(&parsing.root));
245
246    let lowering = if config.depth >= AnalysisDepth::Lowering {
247        parsing
248            .as_ref()
249            .filter(|parsing| parsing.parsing_errors.is_empty())
250            .zip(semantics.as_ref())
251            .filter(|(_, semantics)| semantics.errors.is_empty())
252            .map(|(parsing, _)| lowering::transform_lmntal(&parsing.root))
253    } else {
254        None
255    };
256
257    let ir = if config.depth >= AnalysisDepth::Ir {
258        lowering
259            .as_ref()
260            .filter(|lowering| lowering.errors.is_empty())
261            .map(|lowering| {
262                let mut emitter = Emitter::new();
263                emitter.generate(&lowering.program);
264                emitter.finish()
265            })
266    } else {
267        None
268    };
269
270    DocumentSnapshot {
271        uri,
272        version,
273        source,
274        lexing,
275        parsing,
276        semantics,
277        lowering,
278        ir,
279        language,
280    }
281}
282
283fn document_name(uri: &str) -> String {
284    uri.rsplit('/').next().unwrap_or(uri).to_string()
285}
286
287fn offset_at(source: &Source, line: u32, column: u32) -> Option<usize> {
288    let target_line = line as usize;
289    let target_column = column as usize;
290
291    let mut current_line = 0usize;
292    let mut current_column = 0usize;
293
294    for (offset, ch) in source.source().chars().enumerate() {
295        if current_line == target_line && current_column == target_column {
296            return Some(offset);
297        }
298
299        if ch == '\n' {
300            current_line += 1;
301            current_column = 0;
302        } else {
303            current_column += 1;
304        }
305    }
306
307    if current_line == target_line && current_column == target_column {
308        Some(source.source().chars().count())
309    } else {
310        None
311    }
312}
313
314fn span_contains_offset(span: Span, offset: usize) -> bool {
315    let low = span.low().offset as usize;
316    let high = span.high().offset as usize;
317    if span.is_empty() {
318        low == offset
319    } else {
320        low <= offset && offset < high
321    }
322}
323
324fn collect_membrane_nodes(membrane: &Membrane, nodes: &mut Vec<SyntaxNode>) {
325    nodes.push(SyntaxNode {
326        kind: SyntaxNodeKind::Membrane,
327        span: membrane.span,
328        name: Some(membrane.name.0.clone()),
329    });
330
331    for process_list in &membrane.process_lists {
332        collect_process_list_nodes(process_list, nodes);
333    }
334
335    for rule in &membrane.rules {
336        collect_rule_nodes(rule, nodes);
337    }
338}
339
340fn collect_rule_nodes(rule: &Rule, nodes: &mut Vec<SyntaxNode>) {
341    nodes.push(SyntaxNode {
342        kind: SyntaxNodeKind::Rule,
343        span: rule.span,
344        name: Some(rule.name.0.clone()),
345    });
346    collect_process_list_nodes(&rule.head, nodes);
347    if let Some(propagation) = &rule.propagation {
348        collect_process_list_nodes(propagation, nodes);
349    }
350    if let Some(guard) = &rule.guard {
351        collect_process_list_nodes(guard, nodes);
352    }
353    if let Some(body) = &rule.body {
354        collect_process_list_nodes(body, nodes);
355    }
356}
357
358fn collect_process_list_nodes(process_list: &ProcessList, nodes: &mut Vec<SyntaxNode>) {
359    nodes.push(SyntaxNode {
360        kind: SyntaxNodeKind::ProcessList,
361        span: process_list.span,
362        name: None,
363    });
364
365    for process in &process_list.processes {
366        collect_process_nodes(process, nodes);
367    }
368}
369
370fn collect_process_nodes(process: &Process, nodes: &mut Vec<SyntaxNode>) {
371    match process {
372        Process::Atom(atom) => collect_atom_nodes(atom, nodes),
373        Process::Membrane(membrane) => collect_membrane_nodes(membrane, nodes),
374        Process::Link(link) => nodes.push(link_node(link)),
375        Process::LinkBundle(bundle) => nodes.push(link_bundle_node(bundle)),
376        Process::Hyperlink(hyperlink) => nodes.push(hyperlink_node(hyperlink)),
377        Process::Rule(rule) => collect_rule_nodes(rule, nodes),
378        Process::ProcessContext(context) => nodes.push(process_context_node(context)),
379        Process::RuleContext(context) => nodes.push(rule_context_node(context)),
380    }
381}
382
383fn collect_atom_nodes(atom: &Atom, nodes: &mut Vec<SyntaxNode>) {
384    nodes.push(SyntaxNode {
385        kind: SyntaxNodeKind::Atom,
386        span: atom.span,
387        name: Some(atom.name.0.to_string()),
388    });
389    for arg in &atom.args {
390        collect_process_nodes(arg, nodes);
391    }
392}
393
394fn link_node(link: &Link) -> SyntaxNode {
395    SyntaxNode {
396        kind: SyntaxNodeKind::Link,
397        span: link.span,
398        name: Some(link.name.clone()),
399    }
400}
401
402fn link_bundle_node(bundle: &LinkBundle) -> SyntaxNode {
403    SyntaxNode {
404        kind: SyntaxNodeKind::LinkBundle,
405        span: bundle.span,
406        name: Some(bundle.name.0.clone()),
407    }
408}
409
410fn hyperlink_node(hyperlink: &Hyperlink) -> SyntaxNode {
411    SyntaxNode {
412        kind: SyntaxNodeKind::Hyperlink,
413        span: hyperlink.span,
414        name: Some(hyperlink.name.0.clone()),
415    }
416}
417
418fn process_context_node(context: &ProcessContext) -> SyntaxNode {
419    SyntaxNode {
420        kind: SyntaxNodeKind::ProcessContext,
421        span: context.span,
422        name: Some(context.name.0.clone()),
423    }
424}
425
426fn rule_context_node(context: &RuleContext) -> SyntaxNode {
427    SyntaxNode {
428        kind: SyntaxNodeKind::RuleContext,
429        span: context.span,
430        name: Some(context.name.0.clone()),
431    }
432}
433
434impl PartialOrd for AnalysisDepth {
435    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
436        Some(self.cmp(other))
437    }
438}
439
440impl Ord for AnalysisDepth {
441    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
442        rank(self).cmp(&rank(other))
443    }
444}
445
446fn rank(depth: &AnalysisDepth) -> u8 {
447    match depth {
448        AnalysisDepth::Semantic => 0,
449        AnalysisDepth::Lowering => 1,
450        AnalysisDepth::Ir => 2,
451    }
452}
453
454fn node_specificity(kind: SyntaxNodeKind) -> u8 {
455    match kind {
456        SyntaxNodeKind::Atom
457        | SyntaxNodeKind::Link
458        | SyntaxNodeKind::Hyperlink
459        | SyntaxNodeKind::ProcessContext
460        | SyntaxNodeKind::RuleContext
461        | SyntaxNodeKind::LinkBundle => 0,
462        SyntaxNodeKind::Rule | SyntaxNodeKind::Membrane => 1,
463        SyntaxNodeKind::ProcessList => 2,
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    fn snapshot<'a>(session: &'a AnalysisSession, uri: &str) -> &'a DocumentSnapshot {
472        session.snapshot(uri).expect("snapshot should exist")
473    }
474
475    #[test]
476    fn supports_non_file_uris() {
477        let mut session = AnalysisSession::new();
478        session.set_document("untitled://scratch", 1, "a.");
479
480        let snapshot = snapshot(&session, "untitled://scratch");
481        assert_eq!(snapshot.source().uri(), "untitled://scratch");
482        assert_eq!(snapshot.source().name(), "scratch");
483    }
484
485    #[test]
486    fn open_update_remove_flow_refreshes_snapshots() {
487        let mut session = AnalysisSession::new();
488        session.set_document("file:///test.lmn", 1, "a :-");
489        assert!(
490            session
491                .diagnostics("file:///test.lmn")
492                .iter()
493                .any(|diagnostic| diagnostic.stage == DiagnosticStage::Parsing)
494        );
495
496        session.set_document("file:///test.lmn", 2, "a.");
497        let snapshot = snapshot(&session, "file:///test.lmn");
498        assert_eq!(snapshot.version(), 2);
499        assert!(
500            session
501                .diagnostics("file:///test.lmn")
502                .iter()
503                .all(|diagnostic| diagnostic.stage != DiagnosticStage::Parsing)
504        );
505
506        assert!(session.remove_document("file:///test.lmn").is_some());
507        assert!(session.snapshot("file:///test.lmn").is_none());
508    }
509
510    #[test]
511    fn default_depth_stops_before_lowering_and_ir() {
512        let mut session = AnalysisSession::new();
513        session.set_document("file:///depth.lmn", 1, "a.");
514
515        let snapshot = snapshot(&session, "file:///depth.lmn");
516        assert!(snapshot.semantics().is_some());
517        assert!(snapshot.lowering().is_none());
518        assert!(snapshot.ir().is_none());
519    }
520
521    #[test]
522    fn can_opt_in_to_lowering_and_ir() {
523        let mut session = AnalysisSession::with_config(AnalysisConfig {
524            depth: AnalysisDepth::Ir,
525        });
526        session.set_document("file:///ir.lmn", 1, "name @@ a :- b. a.");
527
528        let snapshot = snapshot(&session, "file:///ir.lmn");
529        assert!(snapshot.lowering().is_some());
530        assert!(snapshot.ir().is_some());
531    }
532
533    #[test]
534    fn outline_generation_includes_init_rules_and_membrane_nesting() {
535        let mut session = AnalysisSession::new();
536        let source = "m{n{a}. inner @@ b :- c}. outer @@ d :- e. a.";
537        session.set_document("file:///outline.lmn", 1, source);
538
539        let outline = snapshot(&session, "file:///outline.lmn").outline();
540        assert_eq!(outline.len(), 2);
541        assert_eq!(outline[0].kind, OutlineKind::InitialProcess);
542        assert_eq!(outline[0].name, "init");
543        assert_eq!(outline[0].children.len(), 1);
544        assert_eq!(outline[0].children[0].kind, OutlineKind::Membrane);
545        assert_eq!(outline[0].children[0].name, "m");
546        assert_eq!(outline[0].children[0].children.len(), 2);
547        assert!(
548            outline[0].children[0]
549                .children
550                .iter()
551                .any(|child| child.kind == OutlineKind::Membrane && child.name == "n")
552        );
553        assert!(
554            outline[0].children[0]
555                .children
556                .iter()
557                .any(|child| child.kind == OutlineKind::Rule && child.name == "inner")
558        );
559        assert_eq!(outline[1].kind, OutlineKind::Rule);
560        assert_eq!(outline[1].name, "outer");
561    }
562
563    #[test]
564    fn semantic_spans_cover_current_categories() {
565        let mut session = AnalysisSession::new();
566        let samples = [
567            ("file:///rule.lmn", "name @@ a :- b."),
568            ("file:///membrane.lmn", "m{a}."),
569            ("file:///atom.lmn", "a."),
570            ("file:///refs.lmn", "a(X,!H), b(X,!H)."),
571            ("file:///context.lmn", "b{@rule, $p[A, B | *K]}."),
572            ("file:///keyword.lmn", "int(A)."),
573            ("file:///operator.lmn", "a(1 + 2)."),
574            ("file:///string.lmn", "a(#\"s\")."),
575            ("file:///number.lmn", "a(1)."),
576        ];
577
578        for (uri, source) in samples {
579            session.set_document(uri, 1, source);
580        }
581
582        let mut kinds = Vec::new();
583        for (uri, _) in samples {
584            kinds.extend(
585                snapshot(&session, uri)
586                    .semantic_spans()
587                    .iter()
588                    .map(|span| span.kind),
589            );
590        }
591
592        assert!(kinds.contains(&SemanticKind::Rule));
593        assert!(kinds.contains(&SemanticKind::Membrane));
594        assert!(kinds.contains(&SemanticKind::Atom));
595        assert!(kinds.contains(&SemanticKind::Link));
596        assert!(kinds.contains(&SemanticKind::Hyperlink));
597        assert!(kinds.contains(&SemanticKind::Context));
598        assert!(kinds.contains(&SemanticKind::KeywordAtom));
599        assert!(kinds.contains(&SemanticKind::OperatorAtom));
600        assert!(kinds.contains(&SemanticKind::StringAtom));
601        assert!(kinds.contains(&SemanticKind::NumberAtom));
602    }
603
604    #[test]
605    fn references_and_highlights_follow_link_and_hyperlink_pairs() {
606        let mut session = AnalysisSession::new();
607        let source = "a(X,!H), b(X,!H).";
608        session.set_document("file:///refs.lmn", 1, source);
609        let snapshot = snapshot(&session, "file:///refs.lmn");
610
611        let link_offset = source.find('X').expect("link offset should exist");
612        let hyperlink_offset = source.find("!H").expect("hyperlink offset should exist");
613
614        assert_eq!(snapshot.references_at_offset(link_offset).len(), 2);
615        assert_eq!(snapshot.highlights_at_offset(link_offset).len(), 2);
616        assert_eq!(snapshot.references_at_offset(hyperlink_offset).len(), 2);
617        assert_eq!(snapshot.highlights_at_offset(hyperlink_offset).len(), 2);
618    }
619
620    #[test]
621    fn exposes_node_queries() {
622        let mut session = AnalysisSession::new();
623        let source = "name @@ a(X) :- b(X). a(1).";
624        session.set_document("file:///symbols.lmn", 1, source);
625
626        let snapshot = snapshot(&session, "file:///symbols.lmn");
627
628        let atom_offset = source.find("b(X)").expect("offset should exist");
629        let node = snapshot
630            .node_at_offset(atom_offset)
631            .expect("node should exist at offset");
632        assert_eq!(node.kind, SyntaxNodeKind::Atom);
633        assert_eq!(node.name.as_deref(), Some("b"));
634
635        let rule_span = snapshot
636            .outline()
637            .iter()
638            .find(|symbol| symbol.kind == OutlineKind::Rule)
639            .expect("rule symbol should exist")
640            .span;
641        let node = snapshot
642            .node_at_span(rule_span)
643            .expect("node should exist at span");
644        assert_eq!(node.kind, SyntaxNodeKind::Rule);
645    }
646
647    #[test]
648    fn converts_position_to_offset() {
649        let mut session = AnalysisSession::new();
650        session.set_document("file:///offset.lmn", 1, "a(\n b).");
651
652        let snapshot = snapshot(&session, "file:///offset.lmn");
653        assert_eq!(snapshot.offset_at(0, 0), Some(0));
654        assert_eq!(snapshot.offset_at(1, 1), Some(4));
655        assert_eq!(snapshot.offset_at(1, 3), Some(6));
656        assert_eq!(snapshot.offset_at(2, 0), None);
657    }
658}