act_seq_dag.py walks an ACT design from an entry .act
file, follows every import (path and namespace) into the stdlib by default,
parses each chp {} / hse {} body, and emits one Graphviz .dot DAG per
body where:
- nodes = statements / parallel-comma clusters / control structures
(loops & selections), inlined so the
;spine of one loop iteration is visible; - edges = the
;operators, each labeled with why it's necessary:RAW(black),WAR(darkred),WAW(purple),CHANNEL(blue) — necessary, keep;SPURIOUS(orange dotted) — no hazard, the;could be a,. These are your optimization targets.
# one file
python3 act_seq_dag.py actnow/tests/wfi_test.act -o wfi.dot --rank
# just one body by name (substring match, case-insensitive)
python3 act_seq_dag.py actnow/soc.act --only soc --rank
# don't descend into std::/sim:: (faster, focus on your own code)
python3 act_seq_dag.py actnow/tests/wfi_test.act --no-stdlib --rank
# machine-readable summary for a dashboard/CI
python3 act_seq_dag.py actnow/tests/wfi_test.act --json wfi.json
# CI gate: fail if any spurious ; remains
python3 act_seq_dag.py actnow/tests/wfi_test.act --fail-on-spurious
# also expand , branches as separate nodes (denser graph)
python3 act_seq_dag.py actnow/soc.act --show-parallel -o soc.dotRender with graphviz:
dot -Tsvg wfi.dot -o wfi.svg # or -Tpng
xdot wfi.dot # interactive=== wfi_test.act: 17 files, 149 bodies, 205 semicolons (72 necessary, 133 spurious) ===
soc chp 55 semi ( 35 nec, 20 spur) soc.act:84 <-- spurious ;'s
mmu chp 16 semi ( 4 nec, 12 spur) mmu.act:49 <-- spurious ;'s
...
Sort by the spur column descending — that's where the most layers can be
removed. necessary is a lower bound on how many ;s you're stuck with
(may be slightly inflated by struct-root matching; see §3).
It mirrors the ACT library's own model: the grammar in act/lang.m4 (the
chp_body/chp_comma_list rules make ; → ACT_CHP_SEMI, , →
ACT_CHP_COMMA) and the conflict checker act/lang_chp.cc:: _check_concurrent_conflicts + _compute_rw_sets. It is a surface parser,
not the compiler:
- It does not elaborate templates, resolve types, or expand
ptype-passed processes. So a templated body appears once (not once per instantiation). That's fine for spotting the pattern, less fine for counting exact gate layers in a fully-expanded netlist. - It matches hazards on the root identifier of dotted names (struct fields
collapse to the struct). This is conservative: it never misses a real
dependency, but it may mark a
;necessary when two disjoint fields of one struct are touched. The tooltip lists the full field names so you can confirm. - It treats every channel action as a write to the channel name (for CHANNEL hazard purposes), matching the compiler's "two parallel branches can't both touch one channel" rule.
When the tool and your intuition disagree, trust the compiler: if actsim
accepts s1, s2 (no conflict warning), the ; was spurious.
Take actnow/mmu.act's main loop. The --rank table says mmu: 16 semi (4 nec, 12 spur). Open mmu.dot, jump to the mmu cluster. The orange
dotted edges are the 12 spurious ;s. Reading them in source order:
addr_core?addr; mode_core?mode; // <-- (1) SPURIOUS: two different channels
op := mode.op; // <-- (2) reads mode.op, writes op
size := mode.size; // <-- (3) SPURIOUS vs (2): disjoint field+var
[ is_write := ... ]; // reads op → necessary after (2)
[ is_read := ... ]; // reads op → spurious vs the is_write select
...
Apply the checklist (§5) to each orange edge:
| edge | S1 writes | S2 reads | S2 writes | shared chan? | verdict | rewrite |
|---|---|---|---|---|---|---|
| (1) | addr, addr_core | – | mode, mode_core | no (diff ch) | SPURIOUS | addr_core?addr, mode_core?mode; |
| (2)→(3) | op | mode.size | size | no | SPURIOUS | op := mode.op, size := mode.size; |
| is_write→is_read | is_write | op | is_read | no | SPURIOUS | [...is_write...], [...is_read...]; (see §9.2 caution) |
The four necessary edges are the channel pairs (addr_mem!addr; rdata_mem?rdata etc.) and the final rdata := mask_data(rdata, size); rdata_core!rdata (RAW on rdata). Those stay ;.
After the rewrite, re-run the tool: the spur count for mmu should drop
toward 0, and the latency of the MMU's per-transaction handshake shrinks by the
removed layers.
- Ran
python3 act_seq_dag.py <entry>.act --rankand read the table. - For each body with
spur > 0, opened the.dot, found the orange edges. - For each orange edge, ran the 4-question checklist (§5) and confirmed no RAW/WAR/WAW/CHANNEL.
- Rewrote the spurious
;to,, parenthesizing for precedence (,binds tighter than;). - Left necessary
;s (channel pairs, real data deps, loop-guard feeds) alone — verified by the tool'sRAW/WAR/WAW/CHANNELlabels. - Re-ran the simulator (
actsim) on the affected testbenches — semantics unchanged, only latency/area improved. - Re-ran the DAG tool:
spurcount went down,necessarycount unchanged (no new hazards introduced).
Golden rule. If you can point at a
;and say specifically which variable read or which channel forces the ordering, keep it. If you can't, it's a candidate — and the DAG tool will already have colored it orange.