Skip to main content

dfir_lang/graph/
flat_graph_builder.rs

1//! Build a flat graph from [`HfStatement`]s.
2
3use std::borrow::Cow;
4use std::collections::btree_map::Entry;
5use std::collections::{BTreeMap, BTreeSet};
6
7use itertools::Itertools;
8use proc_macro2::Span;
9use quote::ToTokens;
10use syn::spanned::Spanned;
11use syn::{Error, Ident, ItemUse};
12
13use crate::diagnostic::{Diagnostic, Diagnostics, Level};
14use crate::graph::meta_graph::ResolvedHandoffRef;
15use crate::graph::ops::{DelayType, FloType, Persistence, PortListSpec, RangeTrait};
16use crate::graph::{
17    DfirGraph, GraphEdgeId, GraphLoopId, GraphNode, GraphNodeId, HandoffKind, PortIndexValue,
18    graph_algorithms,
19};
20use crate::parse::{DfirCode, DfirStatement, Operator, Pipeline};
21use crate::pretty_span::PrettySpan;
22
23#[derive(Clone, Debug)]
24struct Ends {
25    inn: Option<(PortIndexValue, GraphDet)>,
26    out: Option<(PortIndexValue, GraphDet)>,
27}
28
29#[derive(Clone, Debug)]
30enum GraphDet {
31    Determined(GraphNodeId),
32    Undetermined(Ident),
33}
34
35/// Variable name info for each ident, see [`FlatGraphBuilder::varname_ends`].
36#[derive(Debug)]
37struct VarnameInfo {
38    /// What the variable name resolves to.
39    pub ends: Ends,
40    /// Set to true if the varname reference creates an illegal self-referential cycle.
41    pub illegal_cycle: bool,
42    /// Set to true once the in port is used. Used to track unused ports.
43    pub inn_used: bool,
44    /// Set to true once the out port is used. Used to track unused ports.
45    pub out_used: bool,
46}
47impl VarnameInfo {
48    pub fn new(ends: Ends) -> Self {
49        Self {
50            ends,
51            illegal_cycle: false,
52            inn_used: false,
53            out_used: false,
54        }
55    }
56}
57
58/// Wraper around [`DfirGraph`] to build a flat graph from AST code.
59#[derive(Debug, Default)]
60pub struct FlatGraphBuilder {
61    /// Spanned error/warning/etc diagnostics to emit.
62    diagnostics: Diagnostics,
63
64    /// [`DfirGraph`] being built.
65    flat_graph: DfirGraph,
66    /// Variable names, used as [`HfStatement::Named`] are added.
67    varname_ends: BTreeMap<Ident, VarnameInfo>,
68    /// Each (out -> inn) link inputted.
69    links: Vec<Ends>,
70
71    /// Use statements.
72    uses: Vec<ItemUse>,
73
74    /// If the flat graph is being loaded as a module, then two initial ModuleBoundary nodes are inserted into the graph. One
75    /// for the input into the module and one for the output out of the module.
76    module_boundary_nodes: Option<(GraphNodeId, GraphNodeId)>,
77}
78
79/// Output of [`FlatGraphBuilder::build`].
80pub struct FlatGraphBuilderOutput {
81    /// The flat DFIR graph.
82    pub flat_graph: DfirGraph,
83    /// Any `use` statements.
84    pub uses: Vec<ItemUse>,
85    /// Any emitted diagnostics (warnings, errors).
86    pub diagnostics: Diagnostics,
87}
88
89impl FlatGraphBuilder {
90    /// Create a new empty graph builder.
91    pub fn new() -> Self {
92        Default::default()
93    }
94
95    /// Convert the DFIR code AST into a graph builder.
96    pub fn from_dfir(input: DfirCode) -> Self {
97        let mut builder = Self::default();
98        builder.add_dfir(input, None, None);
99        builder
100    }
101
102    /// Build into an unpartitioned [`DfirGraph`], returning a struct containing the flat graph, any diagnostics, and
103    /// other outputs.
104    ///
105    /// If any diagnostics are errors, `Err` is returned and the underlying graph is lost.
106    pub fn build(mut self) -> Result<FlatGraphBuilderOutput, Diagnostics> {
107        self.finalize_connect_operator_links();
108        self.process_operator_errors();
109
110        if self.diagnostics.has_error() {
111            Err(self.diagnostics)
112        } else {
113            Ok(FlatGraphBuilderOutput {
114                flat_graph: self.flat_graph,
115                uses: self.uses,
116                diagnostics: self.diagnostics,
117            })
118        }
119    }
120
121    /// Adds all [`DfirStatement`]s within the [`DfirCode`] to this [`DfirGraph`].
122    ///
123    /// Optional configuration:
124    /// * In the given loop context `current_loop`.
125    /// * With the given operator tag `operator_tag`.
126    pub fn add_dfir(
127        &mut self,
128        dfir: DfirCode,
129        current_loop: Option<GraphLoopId>,
130        operator_tag: Option<&str>,
131    ) {
132        for stmt in dfir.statements {
133            self.add_statement_internal(stmt, current_loop, operator_tag);
134        }
135    }
136
137    /// Add a single [`DfirStatement`] line to this [`DfirGraph`] in the root context.
138    pub fn add_statement(&mut self, stmt: DfirStatement) {
139        self.add_statement_internal(stmt, None, None);
140    }
141
142    /// Programmatically create a new `loop { ... }` context, with the given parent loop context
143    /// (or `None` for a root-level loop).
144    ///
145    /// The returned [`GraphLoopId`] can be passed as the `current_loop` argument of
146    /// [`Self::add_dfir`] / [`Self::append_assign_pipeline`] to place statements inside the loop,
147    ///
148    /// # Panics
149    /// Panics if `parent_loop` is a loop not belonging to this instance.
150    /// across multiple calls. This is the programmatic equivalent of a
151    /// [`DfirStatement::Loop`] block in the surface syntax, but allows the loop body to be built
152    /// up incrementally.
153    pub fn insert_loop(&mut self, parent_loop: Option<GraphLoopId>) -> GraphLoopId {
154        self.flat_graph.insert_loop(parent_loop)
155    }
156
157    /// Add a single [`DfirStatement`] line to this [`DfirGraph`] with given configuration.
158    ///
159    /// Optional configuration:
160    /// * In the given loop context `current_loop`.
161    /// * With the given operator tag `operator_tag`.
162    fn add_statement_internal(
163        &mut self,
164        stmt: DfirStatement,
165        current_loop: Option<GraphLoopId>,
166        operator_tag: Option<&str>,
167    ) {
168        match stmt {
169            DfirStatement::Use(yuse) => {
170                self.uses.push(yuse);
171            }
172            DfirStatement::Named(named) => {
173                let stmt_span = named.span();
174                let ends = self.add_pipeline(
175                    named.pipeline,
176                    Some(&named.name),
177                    current_loop,
178                    operator_tag,
179                );
180                self.assign_varname_checked(named.name, stmt_span, ends);
181            }
182            DfirStatement::Pipeline(pipeline_stmt) => {
183                let ends =
184                    self.add_pipeline(pipeline_stmt.pipeline, None, current_loop, operator_tag);
185                Self::helper_check_unused_port(&mut self.diagnostics, &ends, true);
186                Self::helper_check_unused_port(&mut self.diagnostics, &ends, false);
187            }
188            DfirStatement::Loop(loop_statement) => {
189                let inner_loop = self.flat_graph.insert_loop(current_loop);
190                for stmt in loop_statement.statements {
191                    self.add_statement_internal(stmt, Some(inner_loop), operator_tag);
192                }
193            }
194        }
195    }
196
197    /// Programatically add an pipeline, optionally adding `pred_name` as a single predecessor and
198    /// assigning it all to `asgn_name`.
199    ///
200    /// In DFIR syntax, equivalent to [`Self::add_statement`] of (if all names are supplied):
201    /// ```text
202    /// #asgn_name = #pred_name -> #pipeline;
203    /// ```
204    ///
205    /// But with, optionally:
206    /// * A `current_loop` to put the operator in.
207    /// * An `operator_tag` to tag the operator with, for debugging/tracing.
208    pub fn append_assign_pipeline(
209        &mut self,
210        asgn_name: Option<&Ident>,
211        pred_name: Option<&Ident>,
212        pipeline: Pipeline,
213        current_loop: Option<GraphLoopId>,
214        operator_tag: Option<&str>,
215    ) {
216        let span = pipeline.span();
217        let mut ends = self.add_pipeline(pipeline, asgn_name, current_loop, operator_tag);
218
219        // Connect `pred_name` if supplied.
220        if let Some(pred_name) = pred_name {
221            if let Some(pred_varname_info) = self.varname_ends.get(pred_name) {
222                // Update ends for `asgn_name`.
223                ends = self.connect_ends(pred_varname_info.ends.clone(), ends);
224            } else {
225                self.diagnostics.push(Diagnostic::spanned(
226                    pred_name.span(),
227                    Level::Error,
228                    format!(
229                        "Cannot find referenced name `{}`; name was never assigned.",
230                        pred_name
231                    ),
232                ));
233            }
234        }
235
236        // Assign `asgn_name` if supplied.
237        if let Some(asgn_name) = asgn_name {
238            self.assign_varname_checked(asgn_name.clone(), span, ends);
239        }
240    }
241}
242
243/// Internal methods.
244impl FlatGraphBuilder {
245    /// Assign a variable name to a pipeline, checking for conflicts.
246    fn assign_varname_checked(&mut self, name: Ident, stmt_span: Span, ends: Ends) {
247        match self.varname_ends.entry(name) {
248            Entry::Vacant(vacant_entry) => {
249                vacant_entry.insert(VarnameInfo::new(ends));
250            }
251            Entry::Occupied(occupied_entry) => {
252                let prev_conflict = occupied_entry.key();
253                self.diagnostics.push(Diagnostic::spanned(
254                    prev_conflict.span(),
255                    Level::Error,
256                    format!(
257                        "Existing assignment to `{}` conflicts with later assignment: {} (1/2)",
258                        prev_conflict,
259                        PrettySpan(stmt_span),
260                    ),
261                ));
262                self.diagnostics.push(Diagnostic::spanned(
263                    stmt_span,
264                    Level::Error,
265                    format!(
266                        "Name assignment to `{}` conflicts with existing assignment: {} (2/2)",
267                        prev_conflict,
268                        PrettySpan(prev_conflict.span())
269                    ),
270                ));
271            }
272        }
273    }
274
275    /// Helper: Add a pipeline, i.e. `a -> b -> c`. Return the input and output [`Ends`] for it.
276    fn add_pipeline(
277        &mut self,
278        pipeline: Pipeline,
279        current_varname: Option<&Ident>,
280        current_loop: Option<GraphLoopId>,
281        operator_tag: Option<&str>,
282    ) -> Ends {
283        match pipeline {
284            Pipeline::Paren(ported_pipeline_paren) => {
285                let (inn_port, pipeline_paren, out_port) =
286                    PortIndexValue::from_ported(ported_pipeline_paren);
287                let og_ends = self.add_pipeline(
288                    *pipeline_paren.pipeline,
289                    current_varname,
290                    current_loop,
291                    operator_tag,
292                );
293                Self::helper_combine_ends(&mut self.diagnostics, og_ends, inn_port, out_port)
294            }
295            Pipeline::Name(pipeline_name) => {
296                let (inn_port, ident, out_port) = PortIndexValue::from_ported(pipeline_name);
297
298                // Mingwei: We could lookup non-forward references immediately, but easier to just
299                // have one consistent code path: `GraphDet::Undetermined`.
300                Ends {
301                    inn: Some((inn_port, GraphDet::Undetermined(ident.clone()))),
302                    out: Some((out_port, GraphDet::Undetermined(ident))),
303                }
304            }
305            Pipeline::ModuleBoundary(pipeline_name) => {
306                let Some((input_node, output_node)) = self.module_boundary_nodes else {
307                    self.diagnostics.push(
308                        Error::new(
309                            pipeline_name.span(),
310                            "`mod` is only usable inside of a module.",
311                        )
312                        .into(),
313                    );
314
315                    return Ends {
316                        inn: None,
317                        out: None,
318                    };
319                };
320
321                let (inn_port, _, out_port) = PortIndexValue::from_ported(pipeline_name);
322
323                Ends {
324                    inn: Some((inn_port, GraphDet::Determined(output_node))),
325                    out: Some((out_port, GraphDet::Determined(input_node))),
326                }
327            }
328            Pipeline::Link(pipeline_link) => {
329                // Add the nested LHS and RHS of this link.
330                let lhs_ends = self.add_pipeline(
331                    *pipeline_link.lhs,
332                    current_varname,
333                    current_loop,
334                    operator_tag,
335                );
336                let rhs_ends = self.add_pipeline(
337                    *pipeline_link.rhs,
338                    current_varname,
339                    current_loop,
340                    operator_tag,
341                );
342
343                self.connect_ends(lhs_ends, rhs_ends)
344            }
345            Pipeline::Operator(operator) => {
346                let op_span = Some(operator.span());
347                let (node_id, ends) =
348                    self.add_operator(current_varname, current_loop, operator, op_span);
349                if let Some(operator_tag) = operator_tag {
350                    self.flat_graph
351                        .set_operator_tag(node_id, operator_tag.to_owned());
352                }
353                ends
354            }
355        }
356    }
357
358    /// Connects two [`Ends`] together. Returns the outer [`Ends`] for the connection.
359    ///
360    /// Links the inner ends together by adding it to `self.links`.
361    fn connect_ends(&mut self, lhs_ends: Ends, rhs_ends: Ends) -> Ends {
362        // Outer (first and last) ends.
363        let outer_ends = Ends {
364            inn: lhs_ends.inn,
365            out: rhs_ends.out,
366        };
367        // Inner (link) ends.
368        let link_ends = Ends {
369            out: lhs_ends.out,
370            inn: rhs_ends.inn,
371        };
372        self.links.push(link_ends);
373        outer_ends
374    }
375
376    /// Adds an operator to the graph, returning its [`GraphNodeId`] the input and output [`Ends`] for it.
377    fn add_operator(
378        &mut self,
379        current_varname: Option<&Ident>,
380        current_loop: Option<GraphLoopId>,
381        operator: Operator,
382        op_span: Option<Span>,
383    ) -> (GraphNodeId, Ends) {
384        let node_id = self.flat_graph.insert_node(
385            GraphNode::Operator(operator),
386            current_varname.cloned(),
387            current_loop,
388        );
389        let ends = Ends {
390            inn: Some((
391                PortIndexValue::Elided(op_span),
392                GraphDet::Determined(node_id),
393            )),
394            out: Some((
395                PortIndexValue::Elided(op_span),
396                GraphDet::Determined(node_id),
397            )),
398        };
399        (node_id, ends)
400    }
401
402    /// Connects operator links as a final building step. Processes all the links stored in
403    /// `self.links` and actually puts them into the graph.
404    fn finalize_connect_operator_links(&mut self) {
405        // `->` edges
406        for Ends { out, inn } in std::mem::take(&mut self.links) {
407            let out_opt = Self::helper_resolve_name(
408                &mut self.varname_ends,
409                out,
410                false,
411                &mut self.diagnostics,
412            );
413            let inn_opt =
414                Self::helper_resolve_name(&mut self.varname_ends, inn, true, &mut self.diagnostics);
415            // `None` already have errors in `self.diagnostics`.
416            if let (Some((out_port, out_node)), Some((inn_port, inn_node))) = (out_opt, inn_opt) {
417                let _ = self.finalize_connect_operators(out_port, out_node, inn_port, inn_node);
418            }
419        }
420
421        // Resolve the singleton references for each node.
422        for node_id in self.flat_graph.node_ids().collect::<Vec<_>>() {
423            if let GraphNode::Operator(operator) = self.flat_graph.node(node_id) {
424                let singletons_referenced = operator
425                    .singletons_referenced
426                    .iter()
427                    .map(|singleton_ref| {
428                        let port_det = self
429                            .varname_ends
430                            .get(&singleton_ref.ident)
431                            .filter(|varname_info| !varname_info.illegal_cycle)
432                            .map(|varname_info| &varname_info.ends)
433                            .and_then(|ends| ends.out.as_ref())
434                            .cloned();
435                        let resolved_node_id = if let Some((_port, node_id)) =
436                            Self::helper_resolve_name(
437                                &mut self.varname_ends,
438                                port_det,
439                                false,
440                                &mut self.diagnostics,
441                            ) {
442                            Some(node_id)
443                        } else {
444                            self.diagnostics.push(Diagnostic::spanned(
445                                singleton_ref.span(),
446                                Level::Error,
447                                format!(
448                                    "Cannot find referenced name `{}`; name was never assigned.",
449                                    singleton_ref.ident
450                                ),
451                            ));
452                            None
453                        };
454                        ResolvedHandoffRef {
455                            node_id: resolved_node_id,
456                            is_mut: singleton_ref.token_mut.is_some(),
457                            access_group: singleton_ref.access_group.as_ref().and_then(
458                                |(_, lit_int)| match lit_int.base10_parse::<u32>() {
459                                    Ok(n) => Some(n),
460                                    Err(e) => {
461                                        self.diagnostics.push(Diagnostic::spanned(
462                                            lit_int.span(),
463                                            Level::Error,
464                                            format!("Access group is not a valid `u32`: {}", e),
465                                        ));
466                                        None
467                                    }
468                                },
469                            ),
470                        }
471                    })
472                    .collect();
473
474                self.flat_graph
475                    .set_node_handoff_references(node_id, singletons_referenced);
476            }
477        }
478    }
479
480    /// Recursively resolve a variable name. For handling forward (and backward) name references
481    /// after all names have been assigned.
482    /// Returns `None` if the name is not resolvable, either because it was never assigned or
483    /// because it contains a self-referential cycle.
484    ///
485    /// `is_in` set to `true` means the _input_ side will be returned. `false` means the _output_ side will be returned.
486    fn helper_resolve_name(
487        varname_ends: &mut BTreeMap<Ident, VarnameInfo>,
488        mut port_det: Option<(PortIndexValue, GraphDet)>,
489        is_in: bool,
490        diagnostics: &mut Diagnostics,
491    ) -> Option<(PortIndexValue, GraphNodeId)> {
492        const BACKUP_RECURSION_LIMIT: usize = 1024;
493
494        let mut names = Vec::new();
495        for _ in 0..BACKUP_RECURSION_LIMIT {
496            match port_det? {
497                (port, GraphDet::Determined(node_id)) => {
498                    return Some((port, node_id));
499                }
500                (port, GraphDet::Undetermined(ident)) => {
501                    let Some(varname_info) = varname_ends.get_mut(&ident) else {
502                        diagnostics.push(Diagnostic::spanned(
503                            ident.span(),
504                            Level::Error,
505                            format!("Cannot find name `{}`; name was never assigned.", ident),
506                        ));
507                        return None;
508                    };
509                    // Check for a self-referential cycle.
510                    let cycle_found = names.contains(&ident);
511                    if !cycle_found {
512                        names.push(ident);
513                    };
514                    if cycle_found || varname_info.illegal_cycle {
515                        let len = names.len();
516                        for (i, name) in names.into_iter().enumerate() {
517                            diagnostics.push(Diagnostic::spanned(
518                                name.span(),
519                                Level::Error,
520                                format!(
521                                    "Name `{}` forms or references an illegal self-referential cycle ({}/{}).",
522                                    name,
523                                    i + 1,
524                                    len
525                                ),
526                            ));
527                            // Set value as `Err(())` to trigger `name_ends_result.is_err()`
528                            // diagnostics above if the name is referenced in the future.
529                            varname_ends.get_mut(&name).unwrap().illegal_cycle = true;
530                        }
531                        return None;
532                    }
533
534                    // No self-cycle.
535                    let prev = if is_in {
536                        varname_info.inn_used = true;
537                        &varname_info.ends.inn
538                    } else {
539                        varname_info.out_used = true;
540                        &varname_info.ends.out
541                    };
542                    port_det = Self::helper_combine_end(
543                        diagnostics,
544                        prev.clone(),
545                        port,
546                        if is_in { "input" } else { "output" },
547                    );
548                }
549            }
550        }
551        diagnostics.push(Diagnostic::spanned(
552            Span::call_site(),
553            Level::Error,
554            format!(
555                "Reached the recursion limit {} while resolving names. This is either a dfir bug or you have an absurdly long chain of names: `{}`.",
556                BACKUP_RECURSION_LIMIT,
557                names.iter().map(ToString::to_string).collect::<Vec<_>>().join("` -> `"),
558            )
559        ));
560        None
561    }
562
563    /// Connect two operators on the given port indexes.
564    fn finalize_connect_operators(
565        &mut self,
566        src_port: PortIndexValue,
567        src: GraphNodeId,
568        dst_port: PortIndexValue,
569        dst: GraphNodeId,
570    ) -> GraphEdgeId {
571        {
572            /// Helper to emit conflicts when a port is used twice.
573            fn emit_conflict(
574                inout: &str,
575                old: &PortIndexValue,
576                new: &PortIndexValue,
577                diagnostics: &mut Diagnostics,
578            ) {
579                // TODO(mingwei): Use `MultiSpan` once `proc_macro2` supports it.
580                diagnostics.push(Diagnostic::spanned(
581                    old.span(),
582                    Level::Error,
583                    format!(
584                        "{} connection conflicts with below ({}) (1/2)",
585                        inout,
586                        PrettySpan(new.span()),
587                    ),
588                ));
589                diagnostics.push(Diagnostic::spanned(
590                    new.span(),
591                    Level::Error,
592                    format!(
593                        "{} connection conflicts with above ({}) (2/2)",
594                        inout,
595                        PrettySpan(old.span()),
596                    ),
597                ));
598            }
599
600            // Handle src's successor port conflicts:
601            if src_port.is_specified() {
602                for conflicting_port in self
603                    .flat_graph
604                    .node_successor_edges(src)
605                    .map(|edge_id| self.flat_graph.edge_ports(edge_id).0)
606                    .filter(|&port| port == &src_port)
607                {
608                    emit_conflict("Output", conflicting_port, &src_port, &mut self.diagnostics);
609                }
610            }
611
612            // Handle dst's predecessor port conflicts:
613            if dst_port.is_specified() {
614                for conflicting_port in self
615                    .flat_graph
616                    .node_predecessor_edges(dst)
617                    .map(|edge_id| self.flat_graph.edge_ports(edge_id).1)
618                    .filter(|&port| port == &dst_port)
619                {
620                    emit_conflict("Input", conflicting_port, &dst_port, &mut self.diagnostics);
621                }
622            }
623        }
624        self.flat_graph.insert_edge(src, src_port, dst, dst_port)
625    }
626
627    /// Process operators and emit operator errors.
628    fn process_operator_errors(&mut self) {
629        self.make_operator_instances();
630        self.check_operator_errors();
631        self.warn_unused_port_indexing();
632        self.check_loop_errors();
633    }
634
635    /// Make `OperatorInstance`s for each operator node.
636    fn make_operator_instances(&mut self) {
637        self.flat_graph
638            .insert_node_op_insts_all(&mut self.diagnostics);
639    }
640
641    /// Validates that operators have valid number of inputs, outputs, & arguments.
642    /// Adds errors (and warnings) to `self.diagnostics`.
643    fn check_operator_errors(&mut self) {
644        /// Returns true if an error was found.
645        fn emit_arity_error(
646            op_span: Span,
647            op_name: &str,
648            is_in: bool,
649            is_hard: bool,
650            degree: usize,
651            range: &dyn RangeTrait<usize>,
652            diagnostics: &mut Diagnostics,
653        ) -> bool {
654            let message = format!(
655                "`{}` {} have {} {}, actually has {}.",
656                op_name,
657                if is_hard { "must" } else { "should" },
658                range.human_string(),
659                if is_in { "input(s)" } else { "output(s)" },
660                degree,
661            );
662            let out_of_range = !range.contains(&degree);
663            if out_of_range {
664                diagnostics.push(Diagnostic::spanned(
665                    op_span,
666                    if is_hard {
667                        Level::Error
668                    } else {
669                        Level::Warning
670                    },
671                    message,
672                ));
673            }
674            out_of_range
675        }
676
677        for (node_id, node) in self.flat_graph.nodes() {
678            match node {
679                GraphNode::Operator(operator) => {
680                    let Some(op_inst) = self.flat_graph.node_op_inst(node_id) else {
681                        // Error already emitted by `insert_node_op_insts_all`.
682                        continue;
683                    };
684                    let op_constraints = op_inst.op_constraints;
685                    let op_name = operator.name_string();
686
687                    // Check number of args
688                    if op_constraints.num_args != operator.args.len() {
689                        self.diagnostics.push(Diagnostic::spanned(
690                            operator.span(),
691                            Level::Error,
692                            format!(
693                                "`{}` expects {} argument(s), received {}.",
694                                op_name,
695                                op_constraints.num_args,
696                                operator.args.len()
697                            ),
698                        ));
699                    }
700
701                    // Check input/output (port) arity
702                    let inn_degree = self.flat_graph.node_degree_in(node_id);
703                    let _ = emit_arity_error(
704                        operator.span(),
705                        &op_name,
706                        true,
707                        true,
708                        inn_degree,
709                        op_constraints.hard_range_inn,
710                        &mut self.diagnostics,
711                    ) || emit_arity_error(
712                        operator.span(),
713                        &op_name,
714                        true,
715                        false,
716                        inn_degree,
717                        op_constraints.soft_range_inn,
718                        &mut self.diagnostics,
719                    );
720
721                    let out_degree = self.flat_graph.node_degree_out(node_id);
722                    let _ = emit_arity_error(
723                        operator.span(),
724                        &op_name,
725                        false,
726                        true,
727                        out_degree,
728                        op_constraints.hard_range_out,
729                        &mut self.diagnostics,
730                    ) || emit_arity_error(
731                        operator.span(),
732                        &op_name,
733                        false,
734                        false,
735                        out_degree,
736                        op_constraints.soft_range_out,
737                        &mut self.diagnostics,
738                    );
739
740                    fn emit_port_error<'a>(
741                        op_span: Span,
742                        op_name: &str,
743                        expected_ports_fn: Option<fn() -> PortListSpec>,
744                        actual_ports_iter: impl Iterator<Item = &'a PortIndexValue>,
745                        input_output: &'static str,
746                        diagnostics: &mut Diagnostics,
747                    ) {
748                        let Some(expected_ports_fn) = expected_ports_fn else {
749                            return;
750                        };
751                        let PortListSpec::Fixed(expected_ports) = (expected_ports_fn)() else {
752                            // Separate check inside of `demux` special case.
753                            return;
754                        };
755                        let expected_ports: Vec<_> = expected_ports.into_iter().collect();
756
757                        // Reject unexpected ports.
758                        let ports: BTreeSet<_> = actual_ports_iter
759                            // Use `inspect` before collecting into `BTreeSet` to ensure we get
760                            // both error messages on duplicated port names.
761                            .inspect(|actual_port_iv| {
762                                // For each actually used port `port_index_value`, check if it is expected.
763                                let is_expected = expected_ports.iter().any(|port_index| {
764                                    actual_port_iv == &&port_index.clone().into()
765                                });
766                                // If it is not expected, emit a diagnostic error.
767                                if !is_expected {
768                                    diagnostics.push(Diagnostic::spanned(
769                                        actual_port_iv.span(),
770                                        Level::Error,
771                                        format!(
772                                            "`{}` received unexpected {} port: {}. Expected one of: `{}`",
773                                            op_name,
774                                            input_output,
775                                            actual_port_iv.as_error_message_string(),
776                                            Itertools::intersperse(
777                                                expected_ports
778                                                    .iter()
779                                                    .map(|port| port.to_token_stream().to_string())
780                                                    .map(Cow::Owned),
781                                                Cow::Borrowed("`, `"),
782                                            ).collect::<String>()
783                                        ),
784                                    ))
785                                }
786                            })
787                            .collect();
788
789                        // List missing expected ports.
790                        let missing: Vec<_> = expected_ports
791                            .into_iter()
792                            .filter_map(|expected_port| {
793                                let tokens = expected_port.to_token_stream();
794                                if !ports.contains(&&expected_port.into()) {
795                                    Some(tokens)
796                                } else {
797                                    None
798                                }
799                            })
800                            .collect();
801                        if !missing.is_empty() {
802                            diagnostics.push(Diagnostic::spanned(
803                                op_span,
804                                Level::Error,
805                                format!(
806                                    "`{}` missing expected {} port(s): `{}`.",
807                                    op_name,
808                                    input_output,
809                                    Itertools::intersperse(
810                                        missing.into_iter().map(|port| Cow::Owned(
811                                            port.to_token_stream().to_string()
812                                        )),
813                                        Cow::Borrowed("`, `")
814                                    )
815                                    .collect::<String>()
816                                ),
817                            ));
818                        }
819                    }
820
821                    emit_port_error(
822                        operator.span(),
823                        &op_name,
824                        op_constraints.ports_inn,
825                        self.flat_graph
826                            .node_predecessor_edges(node_id)
827                            .map(|edge_id| self.flat_graph.edge_ports(edge_id).1),
828                        "input",
829                        &mut self.diagnostics,
830                    );
831                    emit_port_error(
832                        operator.span(),
833                        &op_name,
834                        op_constraints.ports_out,
835                        self.flat_graph
836                            .node_successor_edges(node_id)
837                            .map(|edge_id| self.flat_graph.edge_ports(edge_id).0),
838                        "output",
839                        &mut self.diagnostics,
840                    );
841
842                    // Check that singleton references actually reference valid targets.
843                    {
844                        let singletons_resolved = self.flat_graph.node_handoff_references(node_id);
845                        for (resolved_ref, singleton_ref_token) in singletons_resolved
846                            .iter()
847                            .zip_eq(&*operator.singletons_referenced)
848                        {
849                            let Some(singleton_node_id) = resolved_ref.node_id else {
850                                // Error already emitted by `connect_operator_links`, "Cannot find referenced name...".
851                                continue;
852                            };
853                            // Handoff nodes are valid reference targets.
854                            if matches!(
855                                self.flat_graph.node(singleton_node_id),
856                                GraphNode::Handoff { .. },
857                            ) {
858                                continue;
859                            }
860                            let Some(ref_op_inst) = self.flat_graph.node_op_inst(singleton_node_id)
861                            else {
862                                // Error already emitted by `insert_node_op_insts_all`.
863                                continue;
864                            };
865                            let ref_op_constraints = ref_op_inst.op_constraints;
866                            self.diagnostics.push(Diagnostic::spanned(
867                                singleton_ref_token.span(),
868                                Level::Error,
869                                format!(
870                                    "Cannot reference operator `{}`. Use `singleton()`, `optional()`, or `handoff()` to create a referenceable name.",
871                                    ref_op_constraints.name,
872                                ),
873                            ));
874                        }
875                    }
876                }
877                GraphNode::Handoff { kind, src_span, .. } => {
878                    // Validate arity: handoff must have exactly 1 input and 1 output.
879                    let op_name = match kind {
880                        HandoffKind::Vec => "handoff",
881                        HandoffKind::Singleton => "singleton",
882                        HandoffKind::Optional => "optional",
883                    };
884                    let inn_degree = self.flat_graph.node_degree_in(node_id);
885                    emit_arity_error(
886                        *src_span,
887                        op_name,
888                        true,
889                        true,
890                        inn_degree,
891                        &(1..=1),
892                        &mut self.diagnostics,
893                    );
894                    let out_degree = self.flat_graph.node_degree_out(node_id);
895                    emit_arity_error(
896                        *src_span,
897                        op_name,
898                        false,
899                        true,
900                        out_degree,
901                        &(0..=1), // Handoffs may be no-output, for use only by ref.
902                        &mut self.diagnostics,
903                    );
904                }
905                GraphNode::ModuleBoundary { .. } => {
906                    // Module boundaries don't require any checking.
907                }
908            }
909        }
910
911        // Validate singleton references.
912        // All singleton references must have unambiguous group orderings.
913        // Rules:
914        // 1. If any singleton reference has an explicit group number, they all must have one.
915        // 2. Every `#mut` must be in its own group.
916        {
917            let refs_by_target = self.flat_graph.node_handoff_reference_groups();
918            // For each singleton, check the groups.
919            for (_singleton, groups) in refs_by_target {
920                // Rule 1. If any singleton reference has an explicit group number, they all must have one.
921                if 1 < groups.len()
922                    && let Some(ungrouped) = groups.get(&None)
923                {
924                    for &(_src_node, r, span) in ungrouped {
925                        self.diagnostics.push(Diagnostic::spanned(
926                            span,
927                            Level::Error,
928                            format!(
929                                "Must use an explicit group `#{{N}}{}` to reference a singleton when other references use explicit groups.",
930                                if r.is_mut { " mut" } else { "" },
931                            ),
932                        ));
933                    }
934                }
935                // Rule 2. Every `#mut` must be in its own group.
936                for (group_idx, group) in groups {
937                    if 1 < group.len() && group.iter().any(|(_, r, _)| r.is_mut) {
938                        let group_str = if let Some(n) = group_idx {
939                            format!("`#{{{}}}`", n)
940                        } else {
941                            "<default>".to_owned()
942                        };
943                        for (_src_node, _mut_r, span) in
944                            group.into_iter().filter(|(_, r, _)| r.is_mut)
945                        {
946                            self.diagnostics.push(Diagnostic::spanned(
947                                span,
948                                Level::Error,
949                                format!("Mutable singleton references must be the only one in their access group, but group {} has multiple.", group_str),
950                            ));
951                        }
952                    }
953                }
954            }
955        }
956    }
957
958    /// Warns about unused port indexing referenced in [`Self::varname_ends`].
959    /// https://github.com/hydro-project/hydro/issues/1108
960    fn warn_unused_port_indexing(&mut self) {
961        for (_ident, varname_info) in self.varname_ends.iter() {
962            if !varname_info.inn_used {
963                Self::helper_check_unused_port(&mut self.diagnostics, &varname_info.ends, true);
964            }
965            if !varname_info.out_used {
966                Self::helper_check_unused_port(&mut self.diagnostics, &varname_info.ends, false);
967            }
968        }
969    }
970
971    /// Emit a warning to `diagnostics` for an unused port (i.e. if the port is specified for
972    /// reason).
973    fn helper_check_unused_port(diagnostics: &mut Diagnostics, ends: &Ends, is_in: bool) {
974        let port = if is_in { &ends.inn } else { &ends.out };
975        if let Some((port, _)) = port
976            && port.is_specified()
977        {
978            diagnostics.push(Diagnostic::spanned(
979                port.span(),
980                Level::Error,
981                format!(
982                    "{} port index is unused. (Is the port on the correct side?)",
983                    if is_in { "Input" } else { "Output" },
984                ),
985            ));
986        }
987    }
988
989    /// Helper function.
990    /// Combine the port indexing information for indexing wrapped around a name.
991    /// Because the name may already have indexing, this may introduce double indexing (i.e. `[0][0]my_var[0][0]`)
992    /// which would be an error.
993    fn helper_combine_ends(
994        diagnostics: &mut Diagnostics,
995        og_ends: Ends,
996        inn_port: PortIndexValue,
997        out_port: PortIndexValue,
998    ) -> Ends {
999        Ends {
1000            inn: Self::helper_combine_end(diagnostics, og_ends.inn, inn_port, "input"),
1001            out: Self::helper_combine_end(diagnostics, og_ends.out, out_port, "output"),
1002        }
1003    }
1004
1005    /// Helper function.
1006    /// Combine the port indexing info for one input or output.
1007    fn helper_combine_end(
1008        diagnostics: &mut Diagnostics,
1009        og: Option<(PortIndexValue, GraphDet)>,
1010        other: PortIndexValue,
1011        input_output: &'static str,
1012    ) -> Option<(PortIndexValue, GraphDet)> {
1013        // TODO(mingwei): minification pass over this code?
1014
1015        let other_span = other.span();
1016
1017        let (og_port, og_node) = og?;
1018        match og_port.combine(other) {
1019            Ok(combined_port) => Some((combined_port, og_node)),
1020            Err(og_port) => {
1021                // TODO(mingwei): Use `MultiSpan` once `proc_macro2` supports it.
1022                diagnostics.push(Diagnostic::spanned(
1023                    og_port.span(),
1024                    Level::Error,
1025                    format!(
1026                        "Indexing on {} is overwritten below ({}) (1/2).",
1027                        input_output,
1028                        PrettySpan(other_span),
1029                    ),
1030                ));
1031                diagnostics.push(Diagnostic::spanned(
1032                    other_span,
1033                    Level::Error,
1034                    format!(
1035                        "Cannot index on already-indexed {}, previously indexed above ({}) (2/2).",
1036                        input_output,
1037                        PrettySpan(og_port.span()),
1038                    ),
1039                ));
1040                // When errored, just use original and ignore OTHER port to minimize
1041                // noisy/extra diagnostics.
1042                Some((og_port, og_node))
1043            }
1044        }
1045    }
1046
1047    /// Check for loop context-related errors.
1048    fn check_loop_errors(&mut self) {
1049        for (node_id, node) in self.flat_graph.nodes() {
1050            let Some(op_inst) = self.flat_graph.node_op_inst(node_id) else {
1051                continue;
1052            };
1053            let loop_opt = self.flat_graph.node_loop(node_id);
1054
1055            // Ensure no `'tick` or `'static` persistences are used WITHIN a loop context.
1056            // Ensure no `'loop` persistences are used OUTSIDE a loop context.
1057            for persistence in &op_inst.generics.persistence_args {
1058                let span = op_inst.generics.generic_args.span();
1059                match (loop_opt, persistence) {
1060                    (Some(_loop_id), p @ (Persistence::Tick | Persistence::Static)) => {
1061                        self.diagnostics.push(Diagnostic::spanned(
1062                            span,
1063                            Level::Error,
1064                            format!(
1065                                "Operator uses `'{}` persistence, which is not allowed within a `loop {{ ... }}` context.",
1066                                p.to_str_lowercase(),
1067                            ),
1068                        ));
1069                    }
1070                    (None, p @ (Persistence::None | Persistence::Loop)) => {
1071                        self.diagnostics.push(Diagnostic::spanned(
1072                            span,
1073                            Level::Error,
1074                            format!(
1075                                "Operator uses `'{}` persistence, but is not within a `loop {{ ... }}` context.",
1076                                p.to_str_lowercase(),
1077                            ),
1078                        ));
1079                    }
1080                    _ => {}
1081                }
1082            }
1083
1084            // All inputs must be declared in the root block.
1085            if let (Some(_loop_id), Some(FloType::Source)) =
1086                (loop_opt, op_inst.op_constraints.flo_type)
1087            {
1088                self.diagnostics.push(Diagnostic::spanned(
1089                    node.span(),
1090                    Level::Error,
1091                    format!(
1092                        "Source operator `{}(...)` must be at the root level, not within any `loop {{ ... }}` contexts.",
1093                        op_inst.op_constraints.name
1094                    )
1095                ));
1096            }
1097        }
1098
1099        // Check windowing and un-windowing operators, for loop inputs and outputs respectively.
1100        for (_edge_id, (pred_id, node_id)) in self.flat_graph.edges() {
1101            let Some(op_inst) = self.flat_graph.node_op_inst(node_id) else {
1102                continue;
1103            };
1104            let flo_type = &op_inst.op_constraints.flo_type;
1105
1106            let pred_loop_id = self.flat_graph.node_loop(pred_id);
1107            let loop_id = self.flat_graph.node_loop(node_id);
1108
1109            let span = self.flat_graph.node(node_id).span();
1110
1111            let (is_input, is_output) = {
1112                let parent_pred_loop_id =
1113                    pred_loop_id.and_then(|lid| self.flat_graph.loop_parent(lid));
1114                let parent_loop_id = loop_id.and_then(|lid| self.flat_graph.loop_parent(lid));
1115                let is_same = pred_loop_id == loop_id;
1116                let is_input = !is_same && parent_loop_id == pred_loop_id;
1117                let is_output = !is_same && parent_pred_loop_id == loop_id;
1118                if !(is_input || is_output || is_same) {
1119                    self.diagnostics.push(Diagnostic::spanned(
1120                        span,
1121                        Level::Error,
1122                        "Operator input edge may not cross multiple loop contexts.",
1123                    ));
1124                    continue;
1125                }
1126                (is_input, is_output)
1127            };
1128
1129            match flo_type {
1130                None => {
1131                    if is_input {
1132                        self.diagnostics.push(Diagnostic::spanned(
1133                            span,
1134                            Level::Error,
1135                            format!(
1136                                "Operator `{}(...)` entering a loop context must be a windowing operator, but is not.",
1137                                op_inst.op_constraints.name
1138                            )
1139                        ));
1140                    }
1141                    if is_output {
1142                        self.diagnostics.push(Diagnostic::spanned(
1143                            span,
1144                            Level::Error,
1145                            format!(
1146                                "Operator `{}(...)` exiting a loop context must be an un-windowing operator, but is not.",
1147                                op_inst.op_constraints.name
1148                            )
1149                        ));
1150                    }
1151                }
1152                Some(FloType::Windowing | FloType::WindowingLazy) => {
1153                    if !is_input {
1154                        self.diagnostics.push(Diagnostic::spanned(
1155                            span,
1156                            Level::Error,
1157                            format!(
1158                                "Windowing operator `{}(...)` must be the first input operator into a `loop {{ ... }} context.",
1159                                op_inst.op_constraints.name
1160                            )
1161                        ));
1162                    }
1163                }
1164                Some(FloType::Unwindowing) => {
1165                    if !is_output {
1166                        self.diagnostics.push(Diagnostic::spanned(
1167                            span,
1168                            Level::Error,
1169                            format!(
1170                                "Un-windowing operator `{}(...)` must be the first output operator after exiting a `loop {{ ... }} context.",
1171                                op_inst.op_constraints.name
1172                            )
1173                        ));
1174                    }
1175                }
1176                Some(FloType::Source) => {
1177                    // Handled above.
1178                }
1179            }
1180        }
1181
1182        // Must be a DAG (excluding back-edge operators like `defer_tick` / `defer_tick_lazy`).
1183        // TODO(mingwei): Nested loop blocks should count as a single node.
1184        // But this doesn't cause any correctness issues because the nested loops are also DAGs.
1185        for (loop_id, loop_nodes) in self.flat_graph.loops() {
1186            // Filter out defer_tick / defer_tick_lazy operators (they are back-edges).
1187            let filter_back_edges = |&node_id: &GraphNodeId| {
1188                let Some(op_inst) = self.flat_graph.node_op_inst(node_id) else {
1189                    return true;
1190                };
1191                let delay_type =
1192                    (op_inst.op_constraints.input_delaytype_fn)(&PortIndexValue::Elided(None));
1193                !matches!(delay_type, Some(DelayType::Tick | DelayType::TickLazy))
1194            };
1195
1196            let topo_sort_result = graph_algorithms::topo_sort(
1197                loop_nodes.iter().copied().filter(filter_back_edges),
1198                |dst| {
1199                    self.flat_graph
1200                        .node_predecessor_nodes(dst)
1201                        .filter(|&src| Some(loop_id) == self.flat_graph.node_loop(src))
1202                        .filter(filter_back_edges)
1203                },
1204            );
1205            if let Err(cycle) = topo_sort_result {
1206                let len = cycle.len();
1207                for (i, node_id) in cycle.into_iter().enumerate() {
1208                    let span = self.flat_graph.node(node_id).span();
1209                    self.diagnostics.push(Diagnostic::spanned(
1210                        span,
1211                        Level::Error,
1212                        format!(
1213                            "Operator forms an illegal cycle within a `loop {{ ... }}` block. Use `defer_tick()` to pass data across loop iterations. ({}/{})",
1214                            i + 1,
1215                            len,
1216                        ),
1217                    ));
1218                }
1219            }
1220        }
1221    }
1222}
1223
1224#[cfg(test)]
1225mod test {
1226    use syn::parse_quote;
1227
1228    use super::*;
1229
1230    /// Test that [`FlatGraphBuilder::insert_loop`] can be used to programmatically build a loop
1231    /// context, with statements added across multiple [`FlatGraphBuilder::add_dfir`] calls.
1232    #[test]
1233    fn test_insert_loop_programmatic() {
1234        let mut builder = FlatGraphBuilder::new();
1235        builder.add_dfir(
1236            parse_quote! {
1237                inp = source_iter([1, 2, 3]);
1238            },
1239            None,
1240            None,
1241        );
1242        let loop_id = builder.insert_loop(None);
1243        builder.add_dfir(
1244            parse_quote! {
1245                batched = inp -> batch();
1246            },
1247            Some(loop_id),
1248            None,
1249        );
1250        builder.add_dfir(
1251            parse_quote! {
1252                batched -> for_each(std::mem::drop);
1253            },
1254            Some(loop_id),
1255            None,
1256        );
1257
1258        let output = builder.build().unwrap_or_else(|diagnostics| {
1259            panic!("Should build without errors, got: {:?}", diagnostics);
1260        });
1261        let flat_graph = output.flat_graph;
1262
1263        // One root loop, containing the `batch()` and `for_each()` (but not `source_iter()`).
1264        assert_eq!(1, flat_graph.loops().count());
1265        let (built_loop_id, _) = flat_graph.loops().next().unwrap();
1266        assert_eq!(loop_id, built_loop_id);
1267        assert_eq!(None, flat_graph.loop_parent(built_loop_id));
1268
1269        for (node_id, _node) in flat_graph.nodes() {
1270            let Some(op_inst) = flat_graph.node_op_inst(node_id) else {
1271                continue;
1272            };
1273            let expected_loop = match op_inst.op_constraints.name {
1274                "source_iter" => None,
1275                "batch" | "for_each" => Some(built_loop_id),
1276                other => panic!("Unexpected operator: {}", other),
1277            };
1278            assert_eq!(
1279                expected_loop,
1280                flat_graph.node_loop(node_id),
1281                "Wrong loop context for operator `{}`.",
1282                op_inst.op_constraints.name,
1283            );
1284        }
1285    }
1286
1287    /// Test that programmatically-built nested loops have the correct parent relationship.
1288    #[test]
1289    fn test_insert_loop_nested() {
1290        let mut builder = FlatGraphBuilder::new();
1291        let outer_loop = builder.insert_loop(None);
1292        let inner_loop = builder.insert_loop(Some(outer_loop));
1293
1294        builder.add_dfir(
1295            parse_quote! {
1296                inp = source_iter([1, 2, 3]);
1297            },
1298            None,
1299            None,
1300        );
1301        builder.add_dfir(
1302            parse_quote! {
1303                outer = inp -> batch();
1304            },
1305            Some(outer_loop),
1306            None,
1307        );
1308        builder.add_dfir(
1309            parse_quote! {
1310                outer -> batch() -> for_each(std::mem::drop);
1311            },
1312            Some(inner_loop),
1313            None,
1314        );
1315
1316        let output = builder.build().unwrap_or_else(|diagnostics| {
1317            panic!("Should build without errors, got: {:?}", diagnostics);
1318        });
1319        let flat_graph = output.flat_graph;
1320
1321        assert_eq!(2, flat_graph.loops().count());
1322        assert_eq!(None, flat_graph.loop_parent(outer_loop));
1323        assert_eq!(Some(outer_loop), flat_graph.loop_parent(inner_loop));
1324    }
1325
1326    /// Test that loop validation (windowing operator required at loop entry) applies to
1327    /// programmatically-created loop contexts, same as parsed `loop { ... }` blocks.
1328    #[test]
1329    fn test_insert_loop_requires_windowing() {
1330        let mut builder = FlatGraphBuilder::new();
1331        builder.add_dfir(
1332            parse_quote! {
1333                inp = source_iter([1, 2, 3]);
1334            },
1335            None,
1336            None,
1337        );
1338        let loop_id = builder.insert_loop(None);
1339        // `map` is not a windowing operator, so this should fail to build.
1340        builder.add_dfir(
1341            parse_quote! {
1342                inp -> map(|x: usize| x) -> for_each(std::mem::drop);
1343            },
1344            Some(loop_id),
1345            None,
1346        );
1347
1348        let Err(diagnostics) = builder.build() else {
1349            panic!("Should fail to build due to missing windowing operator.");
1350        };
1351        assert!(
1352            diagnostics.iter().any(|diagnostic| {
1353                Level::Error == diagnostic.level
1354                    && diagnostic.message.contains("windowing operator")
1355            }),
1356            "Expected a windowing operator error, got: {:?}",
1357            diagnostics,
1358        );
1359    }
1360}