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}