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