calyx_opt/analysis/
dataflow_order.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
use super::read_write_set::ReadWriteSet;
use crate::analysis;
use calyx_ir::{self as ir};
use calyx_utils::{CalyxResult, Error};
use ir::RRC;
use itertools::Itertools;
use petgraph::{
    algo,
    graph::{DiGraph, NodeIndex},
};
use std::collections::{HashMap, HashSet};

/// Mapping from the name output port to all the input ports that must be driven before it.
type WriteMap = HashMap<ir::Id, HashSet<ir::Id>>;

/// Given a set of assignment, generates an ordering that respects combinatinal
/// dataflow.
pub struct DataflowOrder {
    // Mapping from name of a primitive to its [WriteMap].
    write_map: HashMap<ir::Id, WriteMap>,
}

/// Generate a write map using a primitive definition.
fn prim_to_write_map(prim: &ir::Primitive) -> CalyxResult<WriteMap> {
    let read_together_spec = analysis::PortInterface::comb_path_spec(prim)?;
    let mut inputs = HashSet::new();
    let mut outputs: Vec<(ir::Id, bool)> = Vec::new();

    // Handle ports not mentioned in read_together specs.
    // Each remaining output ports are dependent on all remaining inputs unless it is marked as
    // @stable or is an interface port in which case it does not depend on any inputs.
    for port in &prim.signature {
        let attrs = &port.attributes;
        if attrs.get(ir::NumAttr::ReadTogether).is_some() {
            continue;
        }
        match port.direction {
            ir::Direction::Input => {
                inputs.insert(port.name());
            }
            ir::Direction::Output => outputs.push((
                port.name(),
                attrs
                    .get(ir::BoolAttr::Stable)
                    .or_else(|| attrs.get(ir::NumAttr::Done))
                    .is_some(),
            )),
            ir::Direction::Inout => {
                unreachable!("Primitive ports should not be inout")
            }
        }
    }
    let all_ports: WriteMap = outputs
        .into_iter()
        .map(|(out, stable)| {
            // Stable ports don't depend on anything
            if stable {
                (out, HashSet::new())
            } else {
                (out, inputs.clone())
            }
        })
        .chain(read_together_spec)
        .collect();
    Ok(all_ports)
}

/// Get the name of the port's cell's prototype if it is a component.
fn primitive_parent(pr: &RRC<ir::Port>) -> Option<ir::Id> {
    let port = pr.borrow();
    match &port.cell_parent().borrow().prototype {
        ir::CellType::Primitive { name, .. } => Some(*name),
        ir::CellType::Component { .. }
        | ir::CellType::ThisComponent
        | ir::CellType::Constant { .. } => None,
    }
}

impl DataflowOrder {
    pub fn new<'a>(
        primitives: impl Iterator<Item = &'a ir::Primitive>,
    ) -> CalyxResult<Self> {
        let write_map = primitives
            .map(|p| prim_to_write_map(p).map(|wm| (p.name, wm)))
            .collect::<CalyxResult<_>>()?;
        Ok(DataflowOrder { write_map })
    }

    pub fn dataflow_sort<T>(
        &self,
        assigns: Vec<ir::Assignment<T>>,
    ) -> CalyxResult<Vec<ir::Assignment<T>>>
    where
        T: ToString + Clone + Eq,
    {
        // Construct a graph where a node is an assignment and there is edge between
        // nodes if one should occur before another.
        let mut gr: DiGraph<Option<ir::Assignment<T>>, ()> = DiGraph::new();

        // Mapping from the index corresponding to an assignment to its read/write sets.
        let mut writes: HashMap<ir::Canonical, Vec<NodeIndex>> = HashMap::new();
        let mut reads: Vec<(NodeIndex, (ir::Id, ir::Canonical))> =
            Vec::with_capacity(assigns.len());

        // Assignments to the hole are not considered in the sorting.
        let mut hole_writes: Vec<ir::Assignment<T>> = Vec::new();

        // Construct the nodes that contain the assignments
        for assign in assigns {
            if assign.dst.borrow().is_hole() {
                hole_writes.push(assign)
            } else {
                let rs = ReadWriteSet::port_reads(&assign)
                    .filter_map(|p| {
                        primitive_parent(&p)
                            .map(|comp| (comp, p.borrow().canonical()))
                    })
                    .collect_vec();
                let ws = {
                    let dst = assign.dst.borrow();
                    if dst.cell_parent().borrow().is_primitive::<&str>(None) {
                        Some(dst.canonical())
                    } else {
                        None
                    }
                };
                let idx = gr.add_node(Some(assign));
                reads.extend(rs.into_iter().map(|r| (idx, r)));
                if let Some(w_can) = ws {
                    writes.entry(w_can).or_default().push(idx);
                }
            }
        }

        // Walk over the writes and add edges between all required reads
        // XXX(rachit): This probably adds a bunch of duplicate edges and in the
        // worst case makes this pass much slower than it needs to be.
        for (r_idx, (comp, canonical_port)) in reads {
            let ir::Canonical { cell: inst, port } = canonical_port;
            let dep_ports = self
                .write_map
                .get(&comp)
                .unwrap_or_else(|| {
                    panic!("Component `{}` write map is not defined", comp)
                })
                .get(&port)
                .unwrap_or_else(|| {
                    panic!(
                        "Port `{}.{}` write map is not defined",
                        comp,
                        port.clone()
                    )
                });

            dep_ports
                .iter()
                .cloned()
                .flat_map(|port| writes.get(&ir::Canonical::new(inst, port)))
                .flatten()
                .try_for_each(|w_idx| {
                    if *w_idx == r_idx {
                        Err(Error::misc(format!(
                            "Assignment depends on itself: {}",
                            ir::Printer::assignment_to_str(
                                gr[*w_idx].as_ref().unwrap()
                            )
                        )))
                    } else {
                        gr.add_edge(*w_idx, r_idx, ());
                        Ok(())
                    }
                })?;
        }

        // Generate a topological ordering
        if let Ok(order) = algo::toposort(&gr, None) {
            let mut assigns = order
                .into_iter()
                .map(|idx| gr[idx].take().unwrap())
                .collect_vec();
            assigns.append(&mut hole_writes);
            Ok(assigns)
        } else {
            // Compute strongly connected component of the graph
            let sccs = algo::kosaraju_scc(&gr);
            let scc = sccs
                .iter()
                .find(|cc| cc.len() > 1)
                .expect("All combinational cycles are self loops");
            let msg = scc
                .iter()
                .map(|idx| {
                    ir::Printer::assignment_to_str(gr[*idx].as_ref().unwrap())
                })
                .join("\n");
            Err(Error::misc(format!("Found combinational cycle:\n{}", msg)))
        }
    }
}