Skip to content

Commit bb66790

Browse files
authored
Merge pull request #137 from Pivot-Studio/feat/gen_flow_chart
Feat/gen flow chart
2 parents 93b1720 + f92bde2 commit bb66790

File tree

15 files changed

+764
-2
lines changed

15 files changed

+764
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ out.*
1818
*.asm
1919
*out
2020
*.dSYM
21+
*.dot
22+
dots
2123
.DS_Store
2224
!7zr.exe
2325
*.info

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ dunce = "1.0.3"
3535
toml = "0.5.9"
3636
ariadne = "0.1.5"
3737
dyn-fmt = "0.3.0"
38+
petgraph = "0.6.2"
39+
3840
[dependencies.nom]
3941
version = "7"
4042
features = ["alloc"]

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Compiler](./compiler/README.md)
1818
- [Parser](./compiler/parser.md)
1919
- [AST](./compiler/ast.md)
20+
- [Flow Chart](compiler/flow.md)
2021
- [Language Server](./lsp/README.md)
2122
- [Design](./lsp/design.md)
2223
- [Diagnostic](./lsp/diagnostic.md)

book/src/compiler/flow.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Flow Chart
2+
3+
这是一个附加功能,它将为每个函数生成流程图,以 `.dot` 文件格式输出,`.dot` 文件可通过
4+
- [Graphviz](https://graphviz.org/)
5+
- [Graphviz Online](https://dreampuf.github.io/GraphvizOnline)
6+
7+
查看。
8+
9+
## 依赖
10+
11+
- [petgraph](https://github.com/petgraph/petgraph)
12+
13+
## 实现
14+
15+
流程图的生成包含两个步骤:
16+
17+
- 由 AST 生成 `` 数据结构
18+
- 根据 `` 生成 `.dot文件`
19+
20+
### 图的生成
21+
22+
我们以函数为单位生成 `graph` ,而 AST 根节点为 `ProgramNode`,因此我们需要遍历其 `fntypes`
23+
,逐个生成 `graph` 最终得到一个 `Vec`
24+
25+
```rust,no_run,noplayground
26+
{{#include ../../../src/flow/mod.rs:creategraphs}}
27+
```
28+
29+
接下来实现 `from_ast()` 函数,它接收一个 `NodeEnum`(这是一个 `Statements` 节点), 返回一个完整的 `Graph`,具体分为两步:
30+
- 初步构建图(`build_graph()`)
31+
- 去除不必要节点(`0入度节点`,`虚节点`,`空节点`))
32+
```rust,no_run,noplayground
33+
{{#include ../../../src/flow/mod.rs:fromast}}
34+
```
35+
主要介绍构建图的环节。
36+
37+
38+
39+
定义图的 `节点```
40+
41+
```rust,no_run,noplayground
42+
{{#include ../../../src/flow/mod.rs:nodeandedge}}
43+
```
44+
45+
`build_graph()`函数以 `Statement` 为单位,针对不同节点构建不同的图结构,为了方便的连接节点,我们定义了 `GraphContext` 用于存储上下文信息:
46+
47+
```rust,no_run,noplayground
48+
{{#include ../../../src/flow/mod.rs:GraphContext}}
49+
```
50+
51+
每次调用 `build_graph()` 前,我们需要为构建部分提供两个 `锚点(local_source, local_sink)` ,第一次调用时,`锚点` 即为起点和终点,
52+
以后每次调用前,均需构建两个虚节点,作为`锚点`(虚节点之后将被去掉)。
53+
54+
对于不涉及分支、循环、跳转的简单语句,图结构较为简单:
55+
56+
```
57+
local_source -> current -> local_sink
58+
59+
local_source ------------> local_sink // 注释
60+
61+
local_source ---> ERR ---> local_sink // 错误
62+
```
63+
64+
分支及循环语句则较为复杂(以 `IF语句` 为例):
65+
```
66+
IF: /--Y--> sub_source -----> [...body...] ------> sub_sink ->-\
67+
/ \
68+
local_source -> cond local_sink
69+
\ /
70+
\--N--> sub_source1 -> Option<[...els...]> -> sub_sink ->-/
71+
72+
```
73+
74+
`if.body``if.els` 部分可以通过递归调用 `build_graph()` 构建,但是需要先生成两个虚节点,并**暂时**赋给 `ctx`,构建完毕后,
75+
`ctx``local_source/sink`需要还原
76+
77+
对于语句块,对每个语句调用 `build_graph()` ,每次将 `sub_source` 更改为 `sub_sink``sub_sink` 则重新创建:
78+
```rust,no_run,noplayground
79+
{{#include ../../../src/flow/mod.rs:stsloop}}
80+
```
81+
82+
### .dot 文件生成
83+
84+
我们只需按dot语法格式生成图的点/边的信息即可。下面是一个简单的dot文件:
85+
86+
```
87+
digraph _pointer_struct__name_params {
88+
D0 [shape=box, style=rounded, label="begin", fontname=""];
89+
{rank = sink; D1 [shape=box, style=rounded, label="end", fontname=""];}
90+
D4 [shape=box, label="return\l", fontname=""];
91+
D4 -> D1;
92+
D0 -> D4;
93+
}
94+
```
95+
可以在[Graphviz Online](https://dreampuf.github.io/GraphvizOnline)查看对应流程图。

src/ast/compiler.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use std::{
3232
pub struct Options {
3333
pub genir: bool,
3434
pub printast: bool,
35+
pub flow: bool,
3536
pub optimization: HashOptimizationLevel,
3637
pub fmt: bool,
3738
}
@@ -77,6 +78,7 @@ pub enum ActionType {
7778
Hover,
7879
Compile,
7980
PrintAst,
81+
Flow,
8082
Fmt,
8183
LspFmt,
8284
Hint,
@@ -192,6 +194,11 @@ pub fn compile(db: &dyn Db, docs: MemDocsInput, out: String, op: Options) {
192194
info!("gen source done, time: {:?}", time);
193195
return;
194196
}
197+
if op.flow {
198+
let time = now.elapsed();
199+
info!("gen flow done, time: {:?}", time);
200+
return;
201+
}
195202
let mut mods = compile_dry::accumulated::<ModBuffer>(db, docs);
196203
let mut objs = vec![];
197204
let ctx = Context::create();

src/ast/fmt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use super::{
2929
tokens::TokenType,
3030
};
3131

32+
#[derive(Clone)]
3233
pub struct FmtBuilder {
3334
buf: String,
3435
tabs: usize,

src/ast/node/program.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::ast::compiler::{compile_dry_file, ActionType};
1111
use crate::ast::ctx::{self, Ctx};
1212
use crate::ast::plmod::LSPDef;
1313
use crate::ast::plmod::Mod;
14+
use crate::flow::display::Dot;
1415
use crate::lsp::mem_docs::{EmitParams, MemDocsInput};
1516
use crate::lsp::semantic_tokens::SemanticTokensBuilder;
1617
use crate::lsp::text;
@@ -25,6 +26,7 @@ use rustc_hash::FxHashMap;
2526
use rustc_hash::FxHashSet;
2627
use std::cell::RefCell;
2728
use std::collections::hash_map::DefaultHasher;
29+
use std::fs;
2830
use std::fs::OpenOptions;
2931
use std::hash::{Hash, Hasher};
3032
use std::io::prelude::*;
@@ -240,6 +242,36 @@ impl Program {
240242
let line_index = text::LineIndex::new(&oldcode);
241243
PLFormat::push(db, diff.into_text_edit(&line_index));
242244
}
245+
ActionType::Flow => {
246+
if let NodeEnum::Program(pro) = *nn {
247+
// create dot dir
248+
// let mut dotpath = PathBuf::from(p.dir(db));
249+
let mut dotpath = PathBuf::from("./");
250+
dotpath.push("dots");
251+
if !dotpath.exists() {
252+
std::fs::create_dir(dotpath.clone()).unwrap();
253+
}
254+
let graphs = pro.create_graphs();
255+
for graph in graphs {
256+
let dot = Dot::new(true); // true 曲线; false 折线
257+
let dot_str = dot.generate_from_graph(&graph.graph, &graph.name);
258+
// write to file
259+
let path = format!(
260+
"{}/{}_{}.dot",
261+
dotpath.to_str().unwrap(),
262+
p.fullpath(db)
263+
.replace(|c: char| { !c.is_ascii_alphanumeric() }, "_")
264+
.replace(".pi", ""),
265+
graph.name
266+
);
267+
let mut file = fs::File::create(path.clone()).unwrap();
268+
file.write_all(dot_str.as_bytes()).unwrap();
269+
println!("{} {}", "Written to file".bright_cyan(), path.green());
270+
print!("{}", dot_str);
271+
println!("{}","You can view the flow chart on https://dreampuf.github.io/GraphvizOnline\n".bright_cyan());
272+
}
273+
}
274+
}
243275
_ => {}
244276
}
245277
let m = emit_file(db, p);

src/ast/test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ mod test {
400400
optimization: crate::ast::compiler::HashOptimizationLevel::Aggressive,
401401
genir: true,
402402
printast: false,
403+
flow: false,
403404
fmt: false,
404405
},
405406
);
@@ -430,6 +431,7 @@ mod test {
430431
optimization: crate::ast::compiler::HashOptimizationLevel::Aggressive,
431432
genir: false,
432433
printast: true,
434+
flow: false,
433435
fmt: false,
434436
},
435437
);

src/flow/display.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use super::*;
2+
use petgraph::{
3+
visit::IntoNodeReferences,
4+
visit::{EdgeRef, IntoEdgeReferences},
5+
};
6+
7+
pub struct Dot {
8+
curly: bool,
9+
}
10+
11+
impl Dot {
12+
pub fn new(curly: bool) -> Self {
13+
Dot { curly }
14+
}
15+
pub fn generate_from_graph(&self, graph: &Graph, name: &String) -> String {
16+
let mut res = format!("digraph {} {{\n", name);
17+
if !self.curly {
18+
res.push_str("graph [splines=polyline];\n");
19+
}
20+
for (id, i) in graph.node_references() {
21+
match i {
22+
GraphNodeType::Begin => res.push_str(
23+
format!(
24+
"D{} [shape=box, style=rounded, label=\"begin\", fontname=\"\"];\n",
25+
id.index()
26+
)
27+
.as_str(),
28+
),
29+
GraphNodeType::End => res.push_str(
30+
format!(
31+
"{{rank = sink; D{} [shape=box, style=rounded, label=\"end\", fontname=\"\"];}}\n",
32+
id.index()
33+
)
34+
.as_str(),
35+
),
36+
GraphNodeType::Node(str) => res.push_str(
37+
format!(
38+
"D{} [shape=box, label=\"{}\\l\", fontname=\"\"];\n",
39+
id.index(),
40+
str.replace('\"', "\\\"").replace("\n","\\l")
41+
)
42+
.as_str(),
43+
),
44+
GraphNodeType::Choice(str) => res.push_str(
45+
format!(
46+
"D{} [shape=diamond, label=\"{}?\\l\", fontname=\"\"];\n",
47+
id.index(),
48+
str.replace('\"', "\\\"").replace("\n","\\l")
49+
)
50+
.as_str(),
51+
),
52+
GraphNodeType::Err(src, msg) => res.push_str(
53+
format!(
54+
"D{} [shape=box, label=<<FONT>{}</FONT><BR/><FONT COLOR=\"red\">{}</FONT>>, fontname=\"\"];\n",
55+
id.index(),
56+
src.replace('\"', "\\\"").replace("\n","\\l"),
57+
msg.replace('\"', "\\\"").replace("\n","\\l")
58+
)
59+
.as_str(),
60+
),
61+
// GraphNodeType::Dummy => {} // all dummy node will be eliminated
62+
GraphNodeType::Dummy => {}
63+
}
64+
}
65+
66+
for i in graph.edge_references() {
67+
match i.weight() {
68+
EdgeType::Normal => res.push_str(
69+
format!("D{} -> D{};\n", i.source().index(), i.target().index()).as_str(),
70+
),
71+
EdgeType::Branch(t) => res.push_str(
72+
format!(
73+
"D{}:{} -> D{}:n [xlabel={}];\n",
74+
i.source().index(),
75+
if *t { "s" } else { "e" },
76+
i.target().index(),
77+
if *t { "Y" } else { "N" }
78+
)
79+
.as_str(),
80+
),
81+
};
82+
}
83+
res.push_str("}\n");
84+
res
85+
}
86+
}

0 commit comments

Comments
 (0)