Skip to content

Commit c521670

Browse files
authored
Experimental Python Support
## Language Support - Add initial support for Python ## Visualization - Add support for node clusters. This is used heavily in Python, for context-managers and exception-handling. ## Demo - Add Python support - Add sharing - click the "Share" button to get a sharable link to what you currently see ## Testing - Enable live-testing with the web viewer. Requires that you run both `bun web-tests --watch` and `bun web` at the same time. - By default, `bun web` only shows failing tests - `bun web` color-codes tests to note which are failing ## Extension - No changes ## Known Issues - Backlinks are no longer thicker than normal links. That said, they were half-broken to begin with and were somewhat arbitrary.
2 parents 101db8b + 2b386b4 commit c521670

31 files changed

+5407
-175
lines changed

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88

9+
### Language Support
10+
11+
- Add initial support for Python
12+
13+
### Visualization
14+
15+
- Add support for node clusters. This is used heavily in Python, for context-managers and exception-handling.
16+
17+
### Demo
18+
19+
- Add Python support
20+
- Add sharing - click the "Share" button to get a sharable link to what you currently see
21+
22+
### Testing
23+
24+
- Enable live-testing with the web viewer. Requires that you run both `bun web-tests --watch` and `bun web` at the same time.
25+
- By default, `bun web` only shows failing tests
26+
- `bun web` color-codes tests to note which are failing
27+
28+
### Extension
29+
30+
- No changes
31+
32+
### Known Issues
33+
34+
- Backlinks are no longer thicker than normal links. That said, they were half-broken to begin with and were somewhat arbitrary.
35+
936
## [0.0.4] - 2024-09-10
1037

1138
- Improved comment-test framework to allow writing tests for multiple languages

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Note that the demo only supports a single function and ignores the cursor locati
2020

2121
- Go
2222
- C
23+
- Python (experimental, only in the [interactive demo](https://tmr232.github.io/function-graph-overview/?language=2))
24+
- Since this adds _a lot_ of new visualization types, this is marked "experimental"
25+
as it is very likely to change.
2326

2427
## Development
2528

bun.lockb

1.78 KB
Binary file not shown.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"devDependencies": {
1414
"@codemirror/lang-cpp": "^6.0.2",
1515
"@codemirror/lang-go": "^6.0.1",
16+
"@codemirror/lang-python": "^6.1.6",
1617
"@eslint/js": "^9.9.1",
1718
"@rollup/plugin-wasm": "^6.2.2",
1819
"@sveltejs/vite-plugin-svelte": "^3.1.1",
@@ -22,13 +23,16 @@
2223
"esbuild": "^0.20.0",
2324
"esbuild-plugin-copy": "^2.1.1",
2425
"eslint": "^9.9.1",
26+
"graphology-utils": "^2.5.2",
27+
"lz-string": "^1.5.0",
2528
"prettier": "3.3.3",
2629
"prettier-plugin-svelte": "^3.2.6",
2730
"svelte": "^4.2.18",
2831
"svelte-codemirror-editor": "^1.4.1",
2932
"tree-sitter-c": "^0.23.0",
3033
"tree-sitter-cli": "^0.23.0",
3134
"tree-sitter-go": "^0.23.0",
35+
"tree-sitter-python": "^0.23.2",
3236
"typescript-eslint": "^8.4.0",
3337
"vite": "^5.4.1"
3438
},
@@ -44,7 +48,8 @@
4448
"package": "bun run build && bun run vsce-package",
4549
"publish": "bun run package && bun run vsce-publish",
4650
"clean": "rm -r ./dist",
47-
"web": "bun run ./scripts/collect-comment-tests.ts && bun run --cwd ./src/frontend/ vite",
51+
"web": "bun run --cwd ./src/frontend/ vite",
52+
"web-tests": "bun run ./scripts/collect-comment-tests.ts",
4853
"demo": "bun run --cwd ./src/demo/ vite",
4954
"build-demo": "bun run --cwd ./src/demo/ vite build --outDir ../../dist/demo --base '/function-graph-overview/'",
5055
"format": "bun prettier . --write --log-level silent",
@@ -99,4 +104,4 @@
99104
"engines": {
100105
"vscode": "^1.86.0"
101106
}
102-
}
107+
}

parsers/tree-sitter-go.wasm

16 Bytes
Binary file not shown.

parsers/tree-sitter-python.wasm

445 KB
Binary file not shown.

scripts/collect-comment-tests.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,47 @@
1-
import { testFunctions as testFuncsForGo } from "../src/test/collect-go";
2-
import { testFunctions as testFuncsForC } from "../src/test/collect-c";
31
import { intoRecords } from "../src/test/commentTestUtils";
2+
import { watch } from "fs";
3+
import { parseArgs } from "util";
4+
import { collectTests } from "../src/test/commentTestCollector";
45

5-
const records = intoRecords([...testFuncsForC, ...testFuncsForGo]);
6+
const watchDir = import.meta.dir + "/../src";
67

7-
Bun.write("./dist/tests/commentTests.json", JSON.stringify(records));
8+
const { values } = parseArgs({
9+
args: Bun.argv,
10+
options: {
11+
watch: {
12+
type: "boolean",
13+
default: false,
14+
},
15+
},
16+
strict: true,
17+
allowPositionals: true,
18+
});
19+
20+
async function generateJson() {
21+
try {
22+
const records = intoRecords(await collectTests());
23+
Bun.write("./dist/tests/commentTests.json", JSON.stringify(records));
24+
} catch (error) {
25+
console.log(error);
26+
}
27+
}
28+
29+
generateJson();
30+
if (values.watch) {
31+
const watcher = watch(
32+
watchDir,
33+
{ recursive: true },
34+
async (event, filename) => {
35+
console.log(`${event}: ${filename}, regenerating commentTests.json`);
36+
await generateJson();
37+
},
38+
);
39+
40+
process.on("SIGINT", () => {
41+
// close watcher when Ctrl-C is pressed
42+
console.log("Closing watcher...");
43+
watcher.close();
44+
45+
process.exit(0);
46+
});
47+
}

scripts/generate-parsers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { $ } from "bun";
33
const treeSitter = Bun.file("./node_modules/web-tree-sitter/tree-sitter.wasm");
44
await Bun.write("./parsers/tree-sitter.wasm", treeSitter);
55

6-
const parsers = ["tree-sitter-go", "tree-sitter-c"];
6+
const parsers = ["tree-sitter-go", "tree-sitter-c", "tree-sitter-python"];
77

88
for (const name of parsers) {
99
await $`bun x --bun tree-sitter build --wasm -o ./parsers/${name}.wasm ./node_modules/${name}/`;

src/control-flow/cfg-defs.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { MultiDirectedGraph } from "graphology";
22
import type Parser from "web-tree-sitter";
33

44
export type NodeType =
5+
| "YIELD"
6+
| "THROW"
57
| "MARKER_COMMENT"
68
| "LOOP_HEAD"
79
| "LOOP_EXIT"
@@ -29,12 +31,29 @@ export type NodeType =
2931
| "SWITCH_CONDITION"
3032
| "SWITCH_MERGE"
3133
| "CASE_CONDITION";
32-
export type EdgeType = "regular" | "consequence" | "alternative";
34+
export type EdgeType = "regular" | "consequence" | "alternative" | "exception";
35+
36+
export type ClusterType =
37+
| "with"
38+
| "try"
39+
| "except"
40+
| "else"
41+
| "finally"
42+
| "try-complex";
43+
export type ClusterId = number;
44+
export type Cluster = {
45+
id: ClusterId;
46+
type: ClusterType;
47+
parent?: Cluster;
48+
depth: number;
49+
};
50+
3351
export interface GraphNode {
3452
type: NodeType;
3553
code: string;
3654
lines: number;
3755
markers: string[];
56+
cluster?: Cluster;
3857
}
3958

4059
export interface GraphEdge {
@@ -55,10 +74,13 @@ export interface BasicBlock {
5574
labels?: Map<string, string>;
5675
// Target label
5776
gotos?: Goto[];
77+
// Return statements in the block. Needed for exception handling.
78+
returns?: string[];
5879
}
5980

81+
export type CFGGraph = MultiDirectedGraph<GraphNode, GraphEdge>;
6082
export interface CFG {
61-
graph: MultiDirectedGraph<GraphNode, GraphEdge>;
83+
graph: CFGGraph;
6284
entry: string;
6385
}
6486

@@ -67,7 +89,20 @@ export class BlockHandler {
6789
private continues: string[] = [];
6890
private labels: Map<string, string> = new Map();
6991
private gotos: Array<{ label: string; node: string }> = [];
92+
/**
93+
* All the returns encountered so far.
94+
*
95+
* This is needed for `finally` clauses in exception handling,
96+
* as the return is moved/duplicated to the end of the finally clause.
97+
* This means that when processing returns, we expect to get a new set
98+
* of returns.
99+
*/
100+
private returns: Array<string> = [];
70101

102+
/**
103+
* Operate on all collected breaks and clear them.
104+
* @param callback Handles the breaks, linking them to the relevant nodes.
105+
*/
71106
public forEachBreak(callback: (breakNode: string) => void) {
72107
this.breaks.forEach(callback);
73108
this.breaks = [];
@@ -78,6 +113,10 @@ export class BlockHandler {
78113
this.continues = [];
79114
}
80115

116+
public forEachReturn(callback: (returnNode: string) => string) {
117+
this.returns = this.returns.map(callback);
118+
}
119+
81120
public processGotos(callback: (gotoNode: string, labelNode: string) => void) {
82121
this.gotos.forEach((goto) => {
83122
const labelNode = this.labels.get(goto.label);
@@ -90,9 +129,10 @@ export class BlockHandler {
90129
}
91130

92131
public update(block: BasicBlock): BasicBlock {
93-
this.breaks.push(...(block.breaks || []));
94-
this.continues.push(...(block.continues || []));
95-
this.gotos.push(...(block.gotos || []));
132+
this.breaks.push(...(block.breaks ?? []));
133+
this.continues.push(...(block.continues ?? []));
134+
this.gotos.push(...(block.gotos ?? []));
135+
this.returns.push(...(block.returns ?? []));
96136
block.labels?.forEach((value, key) => this.labels.set(key, value));
97137

98138
return {
@@ -102,16 +142,28 @@ export class BlockHandler {
102142
continues: this.continues,
103143
gotos: this.gotos,
104144
labels: this.labels,
145+
returns: this.returns,
105146
};
106147
}
107148
}
108149

109-
export function mergeNodeAttrs(from: GraphNode, into: GraphNode): GraphNode {
150+
export function mergeNodeAttrs(
151+
from: GraphNode,
152+
into: GraphNode,
153+
): GraphNode | null {
154+
if (from.cluster !== into.cluster) {
155+
return null;
156+
}
157+
const noMergeTypes: NodeType[] = ["YIELD", "THROW"];
158+
if (noMergeTypes.includes(from.type) || noMergeTypes.includes(into.type)) {
159+
return null;
160+
}
110161
return {
111162
type: from.type,
112163
code: `${from.code}\n${into.code}`,
113164
lines: from.lines + into.lines,
114165
markers: [...from.markers, ...into.markers],
166+
cluster: from.cluster,
115167
};
116168
}
117169
export interface Case {

0 commit comments

Comments
 (0)