1use 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#[derive(Debug)]
37struct VarnameInfo {
38 pub ends: Ends,
40 pub illegal_cycle: bool,
42 pub inn_used: bool,
44 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#[derive(Debug, Default)]
60pub struct FlatGraphBuilder {
61 diagnostics: Diagnostics,
63
64 flat_graph: DfirGraph,
66 varname_ends: BTreeMap<Ident, VarnameInfo>,
68 links: Vec<Ends>,
70
71 uses: Vec<ItemUse>,
73
74 module_boundary_nodes: Option<(GraphNodeId, GraphNodeId)>,
77}
78
79pub struct FlatGraphBuilderOutput {
81 pub flat_graph: DfirGraph,
83 pub uses: Vec<ItemUse>,
85 pub diagnostics: Diagnostics,
87}
88
89impl FlatGraphBuilder {
90 pub fn new() -> Self {
92 Default::default()
93 }
94
95 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 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 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 pub fn add_statement(&mut self, stmt: DfirStatement) {
139 self.add_statement_internal(stmt, None, None);
140 }
141
142 pub fn insert_loop(&mut self, parent_loop: Option<GraphLoopId>) -> GraphLoopId {
154 self.flat_graph.insert_loop(parent_loop)
155 }
156
157 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 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 if let Some(pred_name) = pred_name {
221 if let Some(pred_varname_info) = self.varname_ends.get(pred_name) {
222 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 if let Some(asgn_name) = asgn_name {
238 self.assign_varname_checked(asgn_name.clone(), span, ends);
239 }
240 }
241}
242
243impl FlatGraphBuilder {
245 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 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 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 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 fn connect_ends(&mut self, lhs_ends: Ends, rhs_ends: Ends) -> Ends {
362 let outer_ends = Ends {
364 inn: lhs_ends.inn,
365 out: rhs_ends.out,
366 };
367 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 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 fn finalize_connect_operator_links(&mut self) {
405 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 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 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 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 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 varname_ends.get_mut(&name).unwrap().illegal_cycle = true;
530 }
531 return None;
532 }
533
534 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 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 fn emit_conflict(
574 inout: &str,
575 old: &PortIndexValue,
576 new: &PortIndexValue,
577 diagnostics: &mut Diagnostics,
578 ) {
579 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 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 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 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 fn make_operator_instances(&mut self) {
637 self.flat_graph
638 .insert_node_op_insts_all(&mut self.diagnostics);
639 }
640
641 fn check_operator_errors(&mut self) {
644 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(°ree);
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 continue;
683 };
684 let op_constraints = op_inst.op_constraints;
685 let op_name = operator.name_string();
686
687 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 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 return;
754 };
755 let expected_ports: Vec<_> = expected_ports.into_iter().collect();
756
757 let ports: BTreeSet<_> = actual_ports_iter
759 .inspect(|actual_port_iv| {
762 let is_expected = expected_ports.iter().any(|port_index| {
764 actual_port_iv == &&port_index.clone().into()
765 });
766 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 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 {
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 continue;
852 };
853 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 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 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), &mut self.diagnostics,
903 );
904 }
905 GraphNode::ModuleBoundary { .. } => {
906 }
908 }
909 }
910
911 {
917 let refs_by_target = self.flat_graph.node_handoff_reference_groups();
918 for (_singleton, groups) in refs_by_target {
920 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 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 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 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 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 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 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 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 Some((og_port, og_node))
1043 }
1044 }
1045 }
1046
1047 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 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 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 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 }
1179 }
1180 }
1181
1182 for (loop_id, loop_nodes) in self.flat_graph.loops() {
1186 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]
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 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]
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]
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 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}