Skip to main content

lashlang/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6use crate::ast::{AssignPathStep, Declaration, Expr, LabelMetadata, Program, ResourceRefExpr};
7use crate::lexer::Span;
8use crate::tracking::{
9    LashlangAstPath, LashlangExecutionContext, LashlangExecutionSiteBuilder, ProcessBranchSelection,
10};
11use crate::{ModuleArtifact, ModuleRef, ProcessRef};
12
13pub fn static_graph_json(program: &Program, module_ref: impl Into<String>) -> Value {
14    static_graph_for_program(program, module_ref.into())
15}
16
17pub(crate) fn static_graph_json_for_module_ref(
18    module_ref: ModuleRef,
19    process_refs: &BTreeMap<String, ProcessRef>,
20) -> Value {
21    json!({
22        "module_ref": module_ref,
23        "processes": process_refs,
24        "nodes": [],
25        "edges": [],
26    })
27}
28
29pub(crate) fn static_graph_json_without_ir(module_ref: impl Into<String>) -> Value {
30    json!({
31        "module_ref": module_ref.into(),
32        "nodes": [],
33        "edges": [],
34    })
35}
36
37fn static_graph_for_program(program: &Program, module_ref: String) -> Value {
38    let mut nodes = Vec::new();
39    let mut edges = Vec::new();
40
41    for (index, declaration) in program.declarations.iter().enumerate() {
42        let span = program.declaration_spans.get(index).copied();
43        match declaration {
44            Declaration::Process(process) => {
45                let process_id = format!("process:{}", process.name);
46                nodes.push(node(&process_id, "process", process.name.as_str(), span));
47                collect_expr_graph(&process.body, &process_id, span, &mut nodes, &mut edges);
48            }
49            Declaration::Type(type_decl) => {
50                nodes.push(node(
51                    format!("type:{}", type_decl.name),
52                    "type",
53                    type_decl.name.as_str(),
54                    span,
55                ));
56            }
57        }
58    }
59
60    let main_span = program
61        .expression_spans
62        .first()
63        .copied()
64        .or_else(|| program.declaration_spans.last().copied());
65    collect_expr_graph(&program.main, "main", main_span, &mut nodes, &mut edges);
66
67    json!({
68        "module_ref": module_ref,
69        "nodes": nodes,
70        "edges": edges,
71    })
72}
73
74fn collect_expr_graph(
75    expr: &Expr,
76    owner: &str,
77    span: Option<Span>,
78    nodes: &mut Vec<Value>,
79    edges: &mut Vec<Value>,
80) {
81    match expr {
82        Expr::StartProcess(start) => {
83            let target = format!("process:{}", start.process);
84            edges.push(edge(owner, &target, "starts", span));
85            for child in expr.children() {
86                collect_expr_graph(child, owner, span, nodes, edges);
87            }
88        }
89        Expr::SleepFor(_) => {
90            let sleep_id = format!("{owner}:sleep:{}", nodes.len());
91            nodes.push(node(&sleep_id, "sleep", "sleep for", span));
92            edges.push(edge(owner, &sleep_id, "sleeps", span));
93            for child in expr.children() {
94                collect_expr_graph(child, &sleep_id, span, nodes, edges);
95            }
96        }
97        Expr::SleepUntil(_) => {
98            let sleep_id = format!("{owner}:sleep:{}", nodes.len());
99            nodes.push(node(&sleep_id, "sleep", "sleep until", span));
100            edges.push(edge(owner, &sleep_id, "sleeps", span));
101            for child in expr.children() {
102                collect_expr_graph(child, &sleep_id, span, nodes, edges);
103            }
104        }
105        Expr::WaitSignal { name } => {
106            let wait_id = format!("{owner}:wait:{}", nodes.len());
107            nodes.push(node(&wait_id, "wait", format!("wait_signal {name}"), span));
108            edges.push(edge(owner, &wait_id, "waits", span));
109        }
110        Expr::SignalRun { .. } => {
111            let signal_id = format!("{owner}:signal:{}", nodes.len());
112            nodes.push(node(&signal_id, "signal", "signal_run", span));
113            edges.push(edge(owner, &signal_id, "signals", span));
114            for child in expr.children() {
115                collect_expr_graph(child, &signal_id, span, nodes, edges);
116            }
117        }
118        Expr::ReceiverCall { operation, .. } => {
119            let op_id = format!("{owner}:op:{operation}:{}", nodes.len());
120            nodes.push(node(&op_id, "resource_operation", operation.as_str(), span));
121            edges.push(edge(owner, &op_id, "calls", span));
122            for child in expr.children() {
123                collect_expr_graph(child, owner, span, nodes, edges);
124            }
125        }
126        Expr::ResourceRef(resource) => {
127            let resource_id = resource_node_id(resource);
128            nodes.push(node(&resource_id, "resource", resource.path_string(), span));
129            edges.push(edge(owner, resource_id, "uses", span));
130        }
131        Expr::If {
132            condition,
133            then_block,
134            else_block,
135        } => {
136            let branch_id = format!("{owner}:branch:{}", nodes.len());
137            nodes.push(node(&branch_id, "branch", "if", span));
138            edges.push(edge(owner, &branch_id, "branches", span));
139            collect_expr_graph(condition, &branch_id, span, nodes, edges);
140            collect_expr_graph(then_block, &branch_id, span, nodes, edges);
141            collect_expr_graph(else_block, &branch_id, span, nodes, edges);
142        }
143        Expr::Finish(expr) | Expr::Submit(expr) => {
144            let terminal_id = format!("{owner}:terminal:{}", nodes.len());
145            nodes.push(node(&terminal_id, "terminal", "result", span));
146            edges.push(edge(owner, terminal_id, "terminates", span));
147            if let Some(expr) = expr {
148                collect_expr_graph(expr, owner, span, nodes, edges);
149            }
150        }
151        _ => {
152            for child in expr.children() {
153                collect_expr_graph(child, owner, span, nodes, edges);
154            }
155        }
156    }
157}
158
159fn node(
160    id: impl Into<String>,
161    kind: &'static str,
162    label: impl Into<String>,
163    span: Option<Span>,
164) -> Value {
165    json!({
166        "id": id.into(),
167        "kind": kind,
168        "label": label.into(),
169        "span": span_value(span),
170    })
171}
172
173fn edge(
174    from: impl Into<String>,
175    to: impl Into<String>,
176    label: impl Into<String>,
177    span: Option<Span>,
178) -> Value {
179    json!({
180        "from": from.into(),
181        "to": to.into(),
182        "label": label.into(),
183        "span": span_value(span),
184    })
185}
186
187fn span_value(span: Option<Span>) -> Value {
188    let span = span.unwrap_or(Span { start: 0, end: 1 });
189    let end = if span.end > span.start {
190        span.end
191    } else {
192        span.start + 1
193    };
194    json!({ "start": span.start, "end": end })
195}
196
197fn resource_node_id(resource: &ResourceRefExpr) -> String {
198    format!("resource:{}", resource.path_string())
199}
200
201#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
202pub struct LashlangMapOptions {
203    #[serde(default)]
204    pub include_reachable_processes: bool,
205}
206
207#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
208pub struct LashlangMap {
209    pub module_ref: ModuleRef,
210    pub entry_kind: String,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub entry_ref: Option<ProcessRef>,
213    pub entry_name: String,
214    #[serde(default)]
215    pub nodes: Vec<LashlangMapNode>,
216    #[serde(default)]
217    pub edges: Vec<LashlangMapEdge>,
218}
219
220#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
221pub struct LashlangMapNode {
222    pub id: String,
223    pub kind: String,
224    pub label: String,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub label_metadata: Option<LabelMetadata>,
227}
228
229#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
230pub struct LashlangMapEdge {
231    pub id: String,
232    pub from: String,
233    pub to: String,
234    pub label: String,
235}
236
237pub fn map_lashlang_process(
238    artifact: &ModuleArtifact,
239    process_ref: &ProcessRef,
240    options: LashlangMapOptions,
241) -> Option<LashlangMap> {
242    let process_name = artifact.process_name_for_ref(process_ref)?;
243    let mut builder = LashlangMapBuilder {
244        artifact,
245        options,
246        nodes: Vec::new(),
247        edges: Vec::new(),
248        visited_processes: BTreeSet::new(),
249    };
250    builder.visit_process(process_name, LashlangAstPath::root());
251    Some(LashlangMap {
252        module_ref: artifact.module_ref.clone(),
253        entry_kind: "process".to_string(),
254        entry_ref: Some(process_ref.clone()),
255        entry_name: process_name.to_string(),
256        nodes: builder.nodes,
257        edges: builder.edges,
258    })
259}
260
261pub fn map_lashlang_main(artifact: &ModuleArtifact, options: LashlangMapOptions) -> LashlangMap {
262    let mut builder = LashlangMapBuilder {
263        artifact,
264        options,
265        nodes: Vec::new(),
266        edges: Vec::new(),
267        visited_processes: BTreeSet::new(),
268    };
269    builder.visit_main();
270    LashlangMap {
271        module_ref: artifact.module_ref.clone(),
272        entry_kind: "main".to_string(),
273        entry_ref: None,
274        entry_name: "main".to_string(),
275        nodes: builder.nodes,
276        edges: builder.edges,
277    }
278}
279
280struct LashlangMapBuilder<'artifact> {
281    artifact: &'artifact ModuleArtifact,
282    options: LashlangMapOptions,
283    nodes: Vec<LashlangMapNode>,
284    edges: Vec<LashlangMapEdge>,
285    visited_processes: BTreeSet<String>,
286}
287
288impl LashlangMapBuilder<'_> {
289    fn tracking_context(&self, process_name: &str) -> Option<LashlangExecutionContext> {
290        let process_ref = self.artifact.process_ref(process_name)?.clone();
291        Some(LashlangExecutionContext::process(
292            self.artifact.module_ref.clone(),
293            process_ref,
294            process_name,
295        ))
296    }
297
298    fn visit_main(&mut self) {
299        let context = LashlangExecutionContext::main(self.artifact.module_ref.clone());
300        let site_builder = context.builder();
301        let main_id = site_builder.main_node_id();
302        self.node(&main_id, "main", "main", None);
303        self.visit_expr(
304            &self.artifact.canonical_ir.main,
305            &context,
306            std::slice::from_ref(&main_id),
307            LashlangAstPath::root(),
308        );
309    }
310
311    fn visit_process(&mut self, process_name: &str, path: LashlangAstPath) {
312        if !self.visited_processes.insert(process_name.to_string()) {
313            return;
314        }
315        let Some(context) = self.tracking_context(process_name) else {
316            return;
317        };
318        let Some(process) = self.artifact.canonical_ir.process(process_name) else {
319            return;
320        };
321        let site_builder = context.builder();
322        let process_id = site_builder.process_node_id();
323        self.node(&process_id, "process", process_name, process.label.clone());
324        self.visit_expr(
325            &process.body,
326            &context,
327            std::slice::from_ref(&process_id),
328            path,
329        );
330    }
331
332    fn visit_expr(
333        &mut self,
334        expr: &Expr,
335        context: &LashlangExecutionContext,
336        owners: &[String],
337        path: LashlangAstPath,
338    ) -> Vec<String> {
339        self.visit_expr_with_label_metadata(expr, context, owners, path, None)
340    }
341
342    fn visit_expr_with_label_metadata(
343        &mut self,
344        expr: &Expr,
345        context: &LashlangExecutionContext,
346        owners: &[String],
347        path: LashlangAstPath,
348        label_metadata: Option<&LabelMetadata>,
349    ) -> Vec<String> {
350        let site_builder = context.builder();
351        match expr {
352            Expr::LabelAnnotated { label, expr } => {
353                if label_attaches_to_concrete_node(expr) {
354                    self.visit_expr_with_label_metadata(expr, context, owners, path, Some(label))
355                } else {
356                    let site = site_builder.node_site(&path, "step", label.title.as_str());
357                    self.node(
358                        &site.node_id,
359                        &site.node_kind,
360                        &site.label,
361                        Some(label.clone()),
362                    );
363                    self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "steps");
364                    for (index, child) in expr.children().enumerate() {
365                        self.visit_expr(
366                            child,
367                            context,
368                            std::slice::from_ref(&site.node_id),
369                            path.child(index),
370                        );
371                    }
372                    vec![site.node_id]
373                }
374            }
375            Expr::Block(expressions) => {
376                let mut next_owners = owners.to_vec();
377                for (index, expression) in expressions.iter().enumerate() {
378                    next_owners =
379                        self.visit_expr(expression, context, &next_owners, path.child(index));
380                }
381                next_owners
382            }
383            Expr::Assign { target, expr } if label_metadata.is_some() => {
384                let value_index = target
385                    .steps
386                    .iter()
387                    .filter(|step| matches!(step, AssignPathStep::Index(_)))
388                    .count();
389                self.visit_expr_with_label_metadata(
390                    expr,
391                    context,
392                    owners,
393                    path.child(value_index),
394                    label_metadata,
395                )
396            }
397            Expr::Await(expr) | Expr::ResultUnwrap(expr) if label_metadata.is_some() => self
398                .visit_expr_with_label_metadata(
399                    expr,
400                    context,
401                    owners,
402                    path.child(0),
403                    label_metadata,
404                ),
405            Expr::StartProcess(start) => {
406                let site = site_builder.node_site(
407                    &path,
408                    "child_process",
409                    format!("start {}", start.process),
410                );
411                self.node(
412                    &site.node_id,
413                    &site.node_kind,
414                    &site.label,
415                    label_metadata.cloned(),
416                );
417                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "starts");
418                let target = self
419                    .tracking_context(start.process.as_str())
420                    .map(|context| context.builder().process_node_id())
421                    .unwrap_or_else(|| format!("process:{}", start.process));
422                self.edge_with_id(
423                    site_builder.edge_id(&path, &site.node_id, &target, "child"),
424                    &site.node_id,
425                    &target,
426                    "child",
427                );
428                if self.options.include_reachable_processes {
429                    self.visit_process(start.process.as_str(), LashlangAstPath::root());
430                }
431                for (index, child) in expr.children().enumerate() {
432                    self.visit_expr(
433                        child,
434                        context,
435                        std::slice::from_ref(&site.node_id),
436                        path.child(index),
437                    );
438                }
439                vec![site.node_id]
440            }
441            Expr::ReceiverCall { operation, .. } => {
442                let site = site_builder.node_site(&path, "resource_operation", operation.as_str());
443                self.node(
444                    &site.node_id,
445                    &site.node_kind,
446                    &site.label,
447                    label_metadata.cloned(),
448                );
449                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "calls");
450                for (index, child) in expr.children().enumerate() {
451                    self.visit_expr(
452                        child,
453                        context,
454                        std::slice::from_ref(&site.node_id),
455                        path.child(index),
456                    );
457                }
458                vec![site.node_id]
459            }
460            Expr::SleepFor(_) => {
461                let site = site_builder.node_site(&path, "sleep", "sleep for");
462                self.node(
463                    &site.node_id,
464                    &site.node_kind,
465                    &site.label,
466                    label_metadata.cloned(),
467                );
468                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "sleeps");
469                for (index, child) in expr.children().enumerate() {
470                    self.visit_expr(
471                        child,
472                        context,
473                        std::slice::from_ref(&site.node_id),
474                        path.child(index),
475                    );
476                }
477                vec![site.node_id]
478            }
479            Expr::SleepUntil(_) => {
480                let site = site_builder.node_site(&path, "sleep", "sleep until");
481                self.node(
482                    &site.node_id,
483                    &site.node_kind,
484                    &site.label,
485                    label_metadata.cloned(),
486                );
487                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "sleeps");
488                for (index, child) in expr.children().enumerate() {
489                    self.visit_expr(
490                        child,
491                        context,
492                        std::slice::from_ref(&site.node_id),
493                        path.child(index),
494                    );
495                }
496                vec![site.node_id]
497            }
498            Expr::WaitSignal { name } => {
499                let site = site_builder.node_site(&path, "wait", format!("wait_signal {name}"));
500                self.node(
501                    &site.node_id,
502                    &site.node_kind,
503                    &site.label,
504                    label_metadata.cloned(),
505                );
506                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "waits");
507                vec![site.node_id]
508            }
509            Expr::SignalRun { .. } => {
510                let site = site_builder.node_site(&path, "signal", "signal_run");
511                self.node(
512                    &site.node_id,
513                    &site.node_kind,
514                    &site.label,
515                    label_metadata.cloned(),
516                );
517                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "signals");
518                for (index, child) in expr.children().enumerate() {
519                    self.visit_expr(
520                        child,
521                        context,
522                        std::slice::from_ref(&site.node_id),
523                        path.child(index),
524                    );
525                }
526                vec![site.node_id]
527            }
528            Expr::Finish(value) | Expr::Submit(value) => {
529                let site = site_builder.node_site(&path, "terminal", "result");
530                self.node(
531                    &site.node_id,
532                    &site.node_kind,
533                    &site.label,
534                    label_metadata.cloned(),
535                );
536                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "terminates");
537                if let Some(value) = value {
538                    self.visit_expr(
539                        value,
540                        context,
541                        std::slice::from_ref(&site.node_id),
542                        path.child(0),
543                    );
544                }
545                vec![site.node_id]
546            }
547            Expr::Fail(value) => {
548                let site = site_builder.node_site(&path, "terminal", "failure");
549                self.node(
550                    &site.node_id,
551                    &site.node_kind,
552                    &site.label,
553                    label_metadata.cloned(),
554                );
555                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "terminates");
556                self.visit_expr(
557                    value,
558                    context,
559                    std::slice::from_ref(&site.node_id),
560                    path.child(0),
561                );
562                vec![site.node_id]
563            }
564            Expr::ResourceRef(resource) => {
565                let resource_id = resource_node_id(resource);
566                self.node(&resource_id, "resource", &resource.path_string(), None);
567                self.edges_from_owners(&site_builder, &path, owners, &resource_id, "uses");
568                owners.to_vec()
569            }
570            Expr::If {
571                condition,
572                then_block,
573                else_block,
574            } => {
575                let site = site_builder.branch_site(&path);
576                self.node(
577                    &site.node_id,
578                    &site.node_kind,
579                    &site.label,
580                    label_metadata.cloned(),
581                );
582                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "branches");
583                self.visit_expr(
584                    condition,
585                    context,
586                    std::slice::from_ref(&site.node_id),
587                    path.child(0),
588                );
589                let then_path = path.child(1);
590                let else_path = path.child(2);
591                let then_id =
592                    site_builder.branch_arm_node_id(&then_path, ProcessBranchSelection::Then);
593                let else_id =
594                    site_builder.branch_arm_node_id(&else_path, ProcessBranchSelection::Else);
595                self.node(&then_id, "branch_arm", "then", None);
596                self.node(&else_id, "branch_arm", "else", None);
597                if let Some(branch) = &site.branch {
598                    self.edge_with_id(branch.then_edge_id.clone(), &site.node_id, &then_id, "then");
599                    self.edge_with_id(branch.else_edge_id.clone(), &site.node_id, &else_id, "else");
600                }
601                let then_continuations = self.visit_expr(
602                    then_block,
603                    context,
604                    std::slice::from_ref(&then_id),
605                    then_path,
606                );
607                let else_continuations = self.visit_expr(
608                    else_block,
609                    context,
610                    std::slice::from_ref(&else_id),
611                    else_path,
612                );
613                let mut continuations = Vec::new();
614                extend_unique_owners(&mut continuations, then_continuations);
615                extend_unique_owners(&mut continuations, else_continuations);
616                continuations
617            }
618            Expr::Yield(_) => {
619                let site = site_builder.node_site(&path, "process_event", "yield");
620                self.node(
621                    &site.node_id,
622                    &site.node_kind,
623                    &site.label,
624                    label_metadata.cloned(),
625                );
626                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "emits");
627                for (index, child) in expr.children().enumerate() {
628                    self.visit_expr(
629                        child,
630                        context,
631                        std::slice::from_ref(&site.node_id),
632                        path.child(index),
633                    );
634                }
635                vec![site.node_id]
636            }
637            Expr::Wake(_) => {
638                let site = site_builder.node_site(&path, "process_event", "wake");
639                self.node(
640                    &site.node_id,
641                    &site.node_kind,
642                    &site.label,
643                    label_metadata.cloned(),
644                );
645                self.edges_from_owners(&site_builder, &path, owners, &site.node_id, "emits");
646                for (index, child) in expr.children().enumerate() {
647                    self.visit_expr(
648                        child,
649                        context,
650                        std::slice::from_ref(&site.node_id),
651                        path.child(index),
652                    );
653                }
654                vec![site.node_id]
655            }
656            _ => {
657                let mut next_owners = owners.to_vec();
658                for (index, child) in expr.children().enumerate() {
659                    next_owners = self.visit_expr(child, context, &next_owners, path.child(index));
660                }
661                next_owners
662            }
663        }
664    }
665
666    fn node(&mut self, id: &str, kind: &str, label: &str, label_metadata: Option<LabelMetadata>) {
667        if let Some(node) = self.nodes.iter_mut().find(|node| node.id == id) {
668            if node.label_metadata.is_none() && label_metadata.is_some() {
669                node.label_metadata = label_metadata;
670            }
671            return;
672        }
673        self.nodes.push(LashlangMapNode {
674            id: id.to_string(),
675            kind: kind.to_string(),
676            label: label.to_string(),
677            label_metadata,
678        });
679    }
680
681    fn edge_with_id(&mut self, id: String, from: &str, to: &str, label: &str) {
682        if self.edges.iter().any(|edge| edge.id == id) {
683            return;
684        }
685        self.edges.push(LashlangMapEdge {
686            id,
687            from: from.to_string(),
688            to: to.to_string(),
689            label: label.to_string(),
690        });
691    }
692
693    fn edges_from_owners(
694        &mut self,
695        site_builder: &LashlangExecutionSiteBuilder<'_>,
696        path: &LashlangAstPath,
697        owners: &[String],
698        to: &str,
699        label: &str,
700    ) {
701        for owner in owners {
702            self.edge_with_id(
703                site_builder.edge_id(path, owner, to, label),
704                owner,
705                to,
706                label,
707            );
708        }
709    }
710}
711
712fn extend_unique_owners(target: &mut Vec<String>, owners: impl IntoIterator<Item = String>) {
713    for owner in owners {
714        if !target.contains(&owner) {
715            target.push(owner);
716        }
717    }
718}
719
720fn label_attaches_to_concrete_node(expr: &Expr) -> bool {
721    match expr {
722        Expr::LabelAnnotated { .. } => false,
723        Expr::Assign { expr, .. } => label_attaches_to_assignment_value(expr),
724        Expr::Await(expr) | Expr::ResultUnwrap(expr) => label_attaches_to_concrete_node(expr),
725        Expr::ReceiverCall { .. }
726        | Expr::StartProcess(_)
727        | Expr::SleepFor(_)
728        | Expr::SleepUntil(_)
729        | Expr::WaitSignal { .. }
730        | Expr::SignalRun { .. }
731        | Expr::Submit(_)
732        | Expr::Yield(_)
733        | Expr::Wake(_)
734        | Expr::Finish(_)
735        | Expr::Fail(_)
736        | Expr::If { .. } => true,
737        Expr::Block(_)
738        | Expr::Null
739        | Expr::Bool(_)
740        | Expr::Number(_)
741        | Expr::String(_)
742        | Expr::Variable(_)
743        | Expr::Tuple(_)
744        | Expr::List(_)
745        | Expr::ListComprehension { .. }
746        | Expr::Record(_)
747        | Expr::For { .. }
748        | Expr::While { .. }
749        | Expr::Break
750        | Expr::Continue
751        | Expr::ProcessRef { .. }
752        | Expr::HostDescriptorConstructor { .. }
753        | Expr::ResourceRef(_)
754        | Expr::Cancel(_)
755        | Expr::Print(_)
756        | Expr::BuiltinCall { .. }
757        | Expr::Field { .. }
758        | Expr::Index { .. }
759        | Expr::Unary { .. }
760        | Expr::Binary { .. }
761        | Expr::TypeLiteral(_) => false,
762    }
763}
764
765fn label_attaches_to_assignment_value(expr: &Expr) -> bool {
766    match expr {
767        Expr::Await(expr) | Expr::ResultUnwrap(expr) => label_attaches_to_assignment_value(expr),
768        Expr::ReceiverCall { .. }
769        | Expr::StartProcess(_)
770        | Expr::SleepFor(_)
771        | Expr::SleepUntil(_)
772        | Expr::WaitSignal { .. }
773        | Expr::SignalRun { .. }
774        | Expr::Submit(_)
775        | Expr::Yield(_)
776        | Expr::Wake(_)
777        | Expr::Finish(_)
778        | Expr::Fail(_)
779        | Expr::If { .. } => true,
780        _ => false,
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    fn linked(source: &str) -> crate::LinkedModule {
789        let mut resources = crate::LashlangHostCatalog::new();
790        resources.add_module_operation(
791            ["tools"],
792            "Tools",
793            "read_file",
794            "read_file",
795            crate::TypeExpr::Any,
796            crate::TypeExpr::Any,
797        );
798        crate::LinkedModule::link(
799            crate::parse(source).expect("parse module"),
800            crate::LashlangHostEnvironment::new(resources, crate::LashlangAbilities::all())
801                .with_language_features(
802                    crate::LashlangLanguageFeatures::default().with_label_annotations(),
803                ),
804        )
805        .expect("link module")
806    }
807
808    #[test]
809    fn process_map_uses_stable_refs_and_handles_cycles() {
810        let linked = linked(
811            r#"
812            process scan(tool: Tools) {
813              start scan(tool: tool)
814              text = await tool.read_file({ path: "." })?
815              finish text
816            }
817            "#,
818        );
819        let process_ref = linked
820            .artifact
821            .process_ref("scan")
822            .expect("scan process ref")
823            .clone();
824
825        let map = map_lashlang_process(
826            &linked.artifact,
827            &process_ref,
828            LashlangMapOptions {
829                include_reachable_processes: true,
830            },
831        )
832        .expect("map process");
833
834        assert_eq!(map.module_ref, linked.module_ref);
835        assert_eq!(map.entry_ref.as_ref(), Some(&process_ref));
836        assert!(map.nodes.iter().any(|node| node.kind == "process"));
837        assert!(
838            map.nodes
839                .iter()
840                .any(|node| node.kind == "resource_operation")
841        );
842        assert!(map.edges.iter().any(|edge| edge.label == "starts"));
843        assert!(map.edges.iter().all(|edge| !edge.id.is_empty()));
844
845        let remapped = map_lashlang_process(
846            &linked.artifact,
847            &process_ref,
848            LashlangMapOptions {
849                include_reachable_processes: true,
850            },
851        )
852        .expect("remap process");
853        assert_eq!(map, remapped);
854        assert!(
855            map.nodes
856                .iter()
857                .any(|node| node.id.starts_with("resource_operation:")),
858            "resource operation node should use stable hashed identity: {map:?}"
859        );
860    }
861
862    #[test]
863    fn process_map_includes_label_metadata_on_visual_nodes() {
864        let linked = linked(
865            r#"
866            @label(title: "Scan files", description: "Process node")
867            process scan(tool: Tools, flag: bool) {
868              @label(title: "Read file", description: "Operation node")
869              text = await tool.read_file({ path: "." })?
870              @label(title: "Choose path")
871              if flag {
872                @label(title: "Wake agent")
873                wake text
874              } else {
875                @label(title: "Finish scan")
876                finish text
877              }
878            }
879            "#,
880        );
881        let process_ref = linked
882            .artifact
883            .process_ref("scan")
884            .expect("scan process ref")
885            .clone();
886
887        let map = map_lashlang_process(
888            &linked.artifact,
889            &process_ref,
890            LashlangMapOptions::default(),
891        )
892        .expect("map process");
893
894        assert_label_metadata(&map, "process", "Scan files", Some("Process node"));
895        assert_label_metadata(
896            &map,
897            "resource_operation",
898            "Read file",
899            Some("Operation node"),
900        );
901        assert_label_metadata(&map, "branch", "Choose path", None);
902        assert_label_metadata(&map, "process_event", "Wake agent", None);
903        assert_label_metadata(&map, "terminal", "Finish scan", None);
904    }
905
906    #[test]
907    fn main_map_includes_labeled_pure_setup_step_nodes() {
908        let linked = linked(
909            r#"
910            @label(title: "Prepare", description: "Pure setup")
911            value = 1
912            @label(title: "Return")
913            submit value
914            "#,
915        );
916
917        let map = map_lashlang_main(&linked.artifact, LashlangMapOptions::default());
918
919        assert_eq!(map.entry_kind, "main");
920        assert_eq!(map.entry_ref, None);
921        assert_eq!(map.entry_name, "main");
922        assert_label_metadata(&map, "step", "Prepare", Some("Pure setup"));
923        assert_label_metadata(&map, "terminal", "Return", None);
924        let main_id = node_id(&map, "main", "main");
925        let step_id = node_id_with_label_metadata(&map, "step", "Prepare");
926        let terminal_id = node_id_with_label_metadata(&map, "terminal", "Return");
927        assert_edge(&map, &main_id, &step_id, "steps");
928        assert_edge(&map, &step_id, &terminal_id, "terminates");
929    }
930
931    #[test]
932    fn process_map_chains_sequential_visual_statements() {
933        let linked = linked(
934            r#"
935            process on_button(event: any) {
936              @label(title: "Button Pressed")
937              wake event
938              @label(title: "Finish")
939              finish true
940            }
941            "#,
942        );
943        let process_ref = linked
944            .artifact
945            .process_ref("on_button")
946            .expect("on_button process ref")
947            .clone();
948
949        let map = map_lashlang_process(
950            &linked.artifact,
951            &process_ref,
952            LashlangMapOptions::default(),
953        )
954        .expect("map process");
955        let process_id = node_id(&map, "process", "on_button");
956        let wake_id = node_id_with_label_metadata(&map, "process_event", "Button Pressed");
957        let terminal_id = node_id_with_label_metadata(&map, "terminal", "Finish");
958
959        assert_edge(&map, &process_id, &wake_id, "emits");
960        assert_edge(&map, &wake_id, &terminal_id, "terminates");
961        assert!(
962            !map.edges
963                .iter()
964                .any(|edge| edge.from == process_id && edge.to == terminal_id),
965            "terminal should follow wake instead of branching from process: {map:?}"
966        );
967    }
968
969    fn assert_label_metadata(
970        map: &LashlangMap,
971        kind: &str,
972        title: &str,
973        description: Option<&str>,
974    ) {
975        let node = map
976            .nodes
977            .iter()
978            .find(|node| {
979                node.kind == kind
980                    && node
981                        .label_metadata
982                        .as_ref()
983                        .is_some_and(|label| label.title.as_str() == title)
984            })
985            .unwrap_or_else(|| panic!("missing `{title}` {kind} node in {map:?}"));
986        assert_eq!(
987            node.label_metadata
988                .as_ref()
989                .and_then(|label| label.description.as_deref()),
990            description
991        );
992    }
993
994    fn node_id(map: &LashlangMap, kind: &str, label: &str) -> String {
995        map.nodes
996            .iter()
997            .find(|node| node.kind == kind && node.label == label)
998            .unwrap_or_else(|| panic!("missing `{label}` {kind} node in {map:?}"))
999            .id
1000            .clone()
1001    }
1002
1003    fn node_id_with_label_metadata(map: &LashlangMap, kind: &str, title: &str) -> String {
1004        map.nodes
1005            .iter()
1006            .find(|node| {
1007                node.kind == kind
1008                    && node
1009                        .label_metadata
1010                        .as_ref()
1011                        .is_some_and(|label| label.title.as_str() == title)
1012            })
1013            .unwrap_or_else(|| panic!("missing `{title}` {kind} node in {map:?}"))
1014            .id
1015            .clone()
1016    }
1017
1018    fn assert_edge(map: &LashlangMap, from: &str, to: &str, label: &str) {
1019        assert!(
1020            map.edges
1021                .iter()
1022                .any(|edge| edge.from == from && edge.to == to && edge.label == label),
1023            "missing `{label}` edge {from} -> {to} in {map:?}"
1024        );
1025    }
1026
1027    fn assert_no_edge(map: &LashlangMap, from: &str, to: &str, label: &str) {
1028        assert!(
1029            !map.edges
1030                .iter()
1031                .any(|edge| edge.from == from && edge.to == to && edge.label == label),
1032            "unexpected `{label}` edge {from} -> {to} in {map:?}"
1033        );
1034    }
1035
1036    #[test]
1037    fn process_map_joins_value_conditional_continuations_from_branch_arms() {
1038        let linked = linked(
1039            r#"
1040            process choose(tool: Tools, flag: bool) {
1041              topic = flag
1042                ? "red"
1043                : "blue"
1044
1045              @label(title: "Generate Queries")
1046              value = await tool.read_file({ path: topic })?
1047              finish value
1048            }
1049            "#,
1050        );
1051        let process_ref = linked
1052            .artifact
1053            .process_ref("choose")
1054            .expect("choose process ref")
1055            .clone();
1056
1057        let map = map_lashlang_process(
1058            &linked.artifact,
1059            &process_ref,
1060            LashlangMapOptions::default(),
1061        )
1062        .expect("map process");
1063        let branch_id = node_id(&map, "branch", "if");
1064        let then_id = node_id(&map, "branch_arm", "then");
1065        let else_id = node_id(&map, "branch_arm", "else");
1066        let operation_id =
1067            node_id_with_label_metadata(&map, "resource_operation", "Generate Queries");
1068
1069        assert_edge(&map, &then_id, &operation_id, "calls");
1070        assert_edge(&map, &else_id, &operation_id, "calls");
1071        assert_no_edge(&map, &branch_id, &operation_id, "calls");
1072    }
1073
1074    #[test]
1075    fn process_map_joins_block_conditional_continuations_from_branch_bodies() {
1076        let linked = linked(
1077            r#"
1078            process choose(flag: bool) {
1079              if flag {
1080                @label(title: "Then Wake")
1081                wake { path: "then" }
1082              } else {
1083                @label(title: "Else Wake")
1084                wake { path: "else" }
1085              }
1086
1087              @label(title: "Finish")
1088              finish true
1089            }
1090            "#,
1091        );
1092        let process_ref = linked
1093            .artifact
1094            .process_ref("choose")
1095            .expect("choose process ref")
1096            .clone();
1097
1098        let map = map_lashlang_process(
1099            &linked.artifact,
1100            &process_ref,
1101            LashlangMapOptions::default(),
1102        )
1103        .expect("map process");
1104        let branch_id = node_id(&map, "branch", "if");
1105        let then_wake_id = node_id_with_label_metadata(&map, "process_event", "Then Wake");
1106        let else_wake_id = node_id_with_label_metadata(&map, "process_event", "Else Wake");
1107        let terminal_id = node_id_with_label_metadata(&map, "terminal", "Finish");
1108
1109        assert_edge(&map, &then_wake_id, &terminal_id, "terminates");
1110        assert_edge(&map, &else_wake_id, &terminal_id, "terminates");
1111        assert_no_edge(&map, &branch_id, &terminal_id, "terminates");
1112    }
1113
1114    #[test]
1115    fn process_map_has_stable_branch_edges() {
1116        let linked_module = linked(
1117            r#"
1118            process choose(tool: Tools, flag: bool) {
1119              if flag {
1120                value = await tool.read_file({ path: "a" })?
1121                finish value
1122              } else {
1123                finish "none"
1124              }
1125            }
1126            "#,
1127        );
1128        let process_ref = linked_module
1129            .artifact
1130            .process_ref("choose")
1131            .expect("choose process ref")
1132            .clone();
1133
1134        let map = map_lashlang_process(
1135            &linked_module.artifact,
1136            &process_ref,
1137            LashlangMapOptions::default(),
1138        )
1139        .expect("map process");
1140
1141        let branch_edges = map
1142            .edges
1143            .iter()
1144            .filter(|edge| matches!(edge.label.as_str(), "then" | "else"))
1145            .collect::<Vec<_>>();
1146        assert_eq!(branch_edges.len(), 2);
1147        assert!(branch_edges.iter().all(|edge| !edge.id.is_empty()));
1148
1149        let reparsed = linked(
1150            r#"
1151            process choose(tool: Tools, flag: bool) {
1152              if flag {
1153                value = await tool.read_file({ path: "a" })?
1154                finish value
1155              } else {
1156                finish "none"
1157              }
1158            }
1159            "#,
1160        );
1161        let reparsed_ref = reparsed
1162            .artifact
1163            .process_ref("choose")
1164            .expect("choose process ref")
1165            .clone();
1166        let reparsed_map = map_lashlang_process(
1167            &reparsed.artifact,
1168            &reparsed_ref,
1169            LashlangMapOptions::default(),
1170        )
1171        .expect("reparsed map");
1172        assert_eq!(map.nodes, reparsed_map.nodes);
1173        assert_eq!(map.edges, reparsed_map.edges);
1174    }
1175}