From 123483836129c020c9afc43d80a9461c3069061a Mon Sep 17 00:00:00 2001 From: zacheryasc Date: Fri, 6 Feb 2026 14:54:56 +0000 Subject: [PATCH] feat: code DAG visualizer (#11) Visualize the inter-module code dependency DAG --- .gitignore | 2 + README.md | 8 + deps.dot | 215 ++++++ deps.html | 380 ++++++++++ package-lock.json | 18 + package.json | 5 + tools/depgraph/Cargo.lock | 46 ++ tools/depgraph/Cargo.toml | 8 + tools/depgraph/src/main.rs | 1344 ++++++++++++++++++++++++++++++++++++ 9 files changed, 2026 insertions(+) create mode 100644 deps.dot create mode 100644 deps.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tools/depgraph/Cargo.lock create mode 100644 tools/depgraph/Cargo.toml create mode 100644 tools/depgraph/src/main.rs diff --git a/.gitignore b/.gitignore index 42840ac..0bd9521 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target +tools/depgraph/target/ +node_modules/ .vscode/ .venv \ No newline at end of file diff --git a/README.md b/README.md index bcce6fe..78c4ddc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # swactor (S)mall (W)ASM-compatible (actor) library +## Useful + +View code dependency DAG + +```bash +cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps +``` + ## Quick example ```rust diff --git a/deps.dot b/deps.dot new file mode 100644 index 0000000..6b8b355 --- /dev/null +++ b/deps.dot @@ -0,0 +1,215 @@ +digraph swactor { + rankdir=LR; + fontname="Helvetica"; + fontsize=14; + node [fontname="Helvetica", fontsize=11, style=filled, shape=record]; + edge [fontname="Helvetica", fontsize=9]; + label="swactor — internal dependency DAG"; + labelloc=t; + compound=true; + newrank=true; + splines=ortho; + + subgraph cluster_error { + label="error"; + style="rounded,filled"; fillcolor="#f0f0f0"; color="#888"; + Error [label="{Error|0: Box\}", fillcolor="#e8f5e9"]; + } + subgraph cluster_config { + label="config"; + style="rounded,filled"; fillcolor="#f0f0f0"; color="#888"; + BackoffPolicy [label="{BackoffPolicy|spin_threshold: u32\nyield_threshold: u32\nsleep_increment_us: u64\nsleep_max_us: u64}", fillcolor="#e8f5e9"]; + RuntimeConfig [label="{RuntimeConfig|max_actors: usize\nactor_max_messages: usize\nnum_threads: usize\nmailbox_waterlevel: usize\nbackoff_policy: BackoffPolicy}", fillcolor="#e8f5e9"]; + } + subgraph cluster_channel { + label="channel"; + style="rounded,filled"; fillcolor="#f0f0f0"; color="#888"; + HybridChannel [label="{HybridChannel|ring: ArrayQueue\\noverflow: SegQueue\}", fillcolor="#fff9c4"]; + Receiver [label="{Receiver|queue: Arc\\>}", fillcolor="#fff9c4"]; + Sender [label="{Sender|queue: Arc\\>}", fillcolor="#fff9c4"]; + } + subgraph cluster_actor { + label="actor"; + style="rounded,filled"; fillcolor="#e3f2fd"; color="#1565c0"; + Message [label="{«trait» Message}", fillcolor="#bbdefb"]; + ActorInterface [label="{«trait» ActorInterface|Incoming(Message)\nResponse(Message)\nhandle((&self, &Ctx, Self::Incoming))}", fillcolor="#bbdefb"]; + ActorAddress [label="{ActorAddress|0: [u8; ..]}", fillcolor="#bbdefb"]; + Actor [label="{Actor|addr: ActorAddress\nmailbox: Mailbox\\ninner: A}", fillcolor="#bbdefb"]; + AnyActor [label="{«trait» AnyActor|tick((&self, &dyn ContextInner) → bool)\ndeliver((&self, Box\) → bool)}", fillcolor="#bbdefb"]; + } + subgraph cluster_address_map { + label="address_map"; + style="rounded,filled"; fillcolor="#f3e5f5"; color="#7b1fa2"; + WorkerId [label="{WorkerId|0: usize}", fillcolor="#e1bee7"]; + AddressMap [label="{AddressMap|inner: RwLock\\>}", fillcolor="#e1bee7"]; + Placement [label="{Placement|next: AtomicUsize\nnum_workers: usize}", fillcolor="#e1bee7"]; + } + subgraph cluster_runtime { + label="runtime"; + style="rounded,filled"; fillcolor="#fce4ec"; color="#c62828"; + Inbox [label="{Inbox|addr: ActorAddress\ninner: Receiver\}", fillcolor="#ffcdd2"]; + RuntimeHandle [label="{RuntimeHandle|runtime: Arc\\nthreads: Vec\\>}", fillcolor="#ffcdd2"]; + Ctx [label="{Ctx|inner: &'a dyn ContextInner\nself_addr: ActorAddress}", fillcolor="#ffcdd2"]; + SenderT [label="{«trait» SenderT|try_send_any((&self, Box\))}", fillcolor="#ffcdd2"]; + Runtime [label="{Runtime|config: RuntimeConfig\naddress_map: Arc\\ninbox_registry: Arc\\ntransfer_txs: Vec\\>\nspawn_txs: Vec\)\>\>\nplacement: Placement\nis_running: AtomicBool\nsingle_worker: Option\\>\npending_workers: Option\\>}", fillcolor="#ffcdd2"]; + Envelope [label="{Envelope|dest: ActorAddress\npayload: Box\}", fillcolor="#ffcdd2"]; + InboxRegistry [label="{InboxRegistry|senders: RwLock\\>\>}", fillcolor="#ffcdd2"]; + ContextInner [label="{«trait» ContextInner|send_any((&self, ActorAddress, Box\) → Result\<(), Error\>)\nspawn_any((&self, ActorAddress, Box\) → Result\<(), Error\>)\nmailbox_waterlevel((&self) → usize)}", fillcolor="#ffcdd2"]; + } + subgraph cluster_worker { + label="worker"; + style="rounded,filled"; fillcolor="#fff3e0"; color="#e65100"; + TickContext [label="{TickContext|address_map: &'a AddressMap\ntransfer_txs: &'a [Sender\]\nspawn_txs: &'a [Sender\<(ActorAddress, Box\)\>]\nplacement: &'a Placement\ninbox_registry: &'a InboxRegistry\nconfig: &'a RuntimeConfig}", fillcolor="#ffe0b2"]; + Worker [label="{Worker|id: WorkerId\npool: ActorPool\ntransfer_rx: Receiver\\nspawn_rx: Receiver\<(ActorAddress, Box\)\>}", fillcolor="#ffe0b2"]; + WorkerContext [label="{WorkerContext|worker_id: WorkerId\naddress_map: &'a AddressMap\ntransfer_txs: &'a [Sender\]\nspawn_txs: &'a [Sender\<(ActorAddress, Box\)\>]\nplacement: &'a Placement\ninbox_registry: &'a InboxRegistry\nconfig: &'a RuntimeConfig\npending_local: &'a RefCell\)\>\>}", fillcolor="#ffe0b2"]; + ActorPool [label="{ActorPool|actors: HashMap\\>}", fillcolor="#ffe0b2"]; + Mailbox [label="{Mailbox|queue: VecDeque\\nwaterlevel: usize}", fillcolor="#ffe0b2"]; + } + subgraph cluster_python { + label="python (feature-gated)"; + style="rounded,dashed,filled"; fillcolor="#f5f5f5"; color="#999"; + PyMsg [label="{PyMsg|0: PyObject}", fillcolor="#d7ccc8"]; + PyActorAddress [label="{PyActorAddress|inner: ActorAddress}", fillcolor="#d7ccc8"]; + Effect [label="{«enum» Effect|Send \{ addr: ActorAddress, msg: PyObject \}\nSpawn \{ addr: ActorAddress, handler: PyObject \}}", fillcolor="#d7ccc8"]; + PyCtx [label="{PyCtx|self_addr: ActorAddress\neffects: RefCell\\>}", fillcolor="#d7ccc8"]; + PyActor [label="{PyActor|handler: PyObject}", fillcolor="#d7ccc8"]; + PyInbox [label="{PyInbox|inner: Inbox\}", fillcolor="#d7ccc8"]; + PyRuntimeConfig [label="{PyRuntimeConfig|num_threads: usize\nmax_actors: usize\nactor_max_messages: usize\nmailbox_waterlevel: usize\nspin_threshold: u32\nyield_threshold: u32\nsleep_increment_us: u64\nsleep_max_us: u64}", fillcolor="#d7ccc8"]; + PyRuntime [label="{PyRuntime|inner: Option\}", fillcolor="#d7ccc8"]; + PyRuntimeHandle [label="{PyRuntimeHandle|inner: Option\}", fillcolor="#d7ccc8"]; + } + + // ═══════════════════════════════════════════════════════════════════ + // INTRA-MODULE EDGES (within same cluster) + // ═══════════════════════════════════════════════════════════════════ + + ActorInterface -> Message [label="Incoming", style=dashed, color="#666", penwidth=1]; + Actor -> ActorAddress [label="addr", style=dashed, color="#666", penwidth=1]; + Actor -> AnyActor [label="impl", style=dotted, color="#666", penwidth=1]; + Worker -> ActorPool [label="pool", style=dashed, color="#666", penwidth=1]; + Worker -> TickContext [label="tick_once() param", style=dashed, color="#666", penwidth=1]; + Receiver -> HybridChannel [label="queue", style=dashed, color="#666", penwidth=1]; + Sender -> HybridChannel [label="queue", style=dashed, color="#666", penwidth=1]; + Receiver -> Sender [label="new_sender() param", style=dashed, color="#666", penwidth=1]; + AddressMap -> WorkerId [label="inner", style=dashed, color="#666", penwidth=1]; + Placement -> WorkerId [label="next_worker() param", style=dashed, color="#666", penwidth=1]; + RuntimeConfig -> BackoffPolicy [label="backoff_policy", style=dashed, color="#666", penwidth=1]; + RuntimeHandle -> Runtime [label="runtime", style=dashed, color="#666", penwidth=1]; + Ctx -> ContextInner [label="inner", style=dashed, color="#666", penwidth=1]; + Runtime -> InboxRegistry [label="inbox_registry", style=dashed, color="#666", penwidth=1]; + Runtime -> Envelope [label="transfer_txs", style=dashed, color="#666", penwidth=1]; + InboxRegistry -> SenderT [label="senders", style=dashed, color="#666", penwidth=1]; + Runtime -> Inbox [label="new_inbox() param", style=dashed, color="#666", penwidth=1]; + Runtime -> RuntimeHandle [label="run() param", style=dashed, color="#666", penwidth=1]; + Runtime -> ContextInner [label="impl", style=dotted, color="#666", penwidth=1]; + PyCtx -> Effect [label="effects", style=dashed, color="#666", penwidth=1]; + PyInbox -> PyMsg [label="inner", style=dashed, color="#666", penwidth=1]; + PyCtx -> PyActorAddress [label="self_addr() param", style=dashed, color="#666", penwidth=1]; + PyInbox -> PyActorAddress [label="addr() param", style=dashed, color="#666", penwidth=1]; + PyRuntime -> PyRuntimeConfig [label="new() param", style=dashed, color="#666", penwidth=1]; + PyRuntime -> PyActorAddress [label="spawn() param", style=dashed, color="#666", penwidth=1]; + PyRuntime -> PyInbox [label="inbox() param", style=dashed, color="#666", penwidth=1]; + PyRuntime -> PyRuntimeHandle [label="run() param", style=dashed, color="#666", penwidth=1]; + PyRuntimeHandle -> PyActorAddress [label="spawn() param", style=dashed, color="#666", penwidth=1]; + PyRuntimeHandle -> PyInbox [label="inbox() param", style=dashed, color="#666", penwidth=1]; + + // ═══════════════════════════════════════════════════════════════════ + // CROSS-MODULE EDGES (the real dependency DAG) + // ═══════════════════════════════════════════════════════════════════ + + // --- channel depends on runtime --- + Sender -> SenderT [label="impl", style=dotted, color="#666", penwidth=1.5]; + + // --- actor depends on runtime --- + ActorInterface -> Ctx [label="handle", style=solid, color="#1565c0", penwidth=1.5]; + AnyActor -> ContextInner [label="tick", style=solid, color="#1565c0", penwidth=1.5]; + Actor -> ContextInner [label="tick() param", style=solid, color="#1565c0", penwidth=1.5]; + + // --- actor depends on worker --- + Actor -> Mailbox [label="mailbox", style=solid, color="#1565c0", penwidth=1.5]; + + // --- address_map depends on actor --- + AddressMap -> ActorAddress [label="inner", style=solid, color="#7b1fa2", penwidth=1.5]; + + // --- runtime depends on error --- + ContextInner -> Error [label="send_any", style=solid, color="#c62828", penwidth=1.5]; + Ctx -> Error [label="send() param", style=solid, color="#c62828", penwidth=1.5]; + Runtime -> Error [label="spawn() param", style=solid, color="#c62828", penwidth=1.5]; + InboxRegistry -> Error [label="try_deliver() param", style=solid, color="#c62828", penwidth=1.5]; + + // --- runtime depends on config --- + Runtime -> RuntimeConfig [label="config", style=solid, color="#c62828", penwidth=1.5]; + + // --- runtime depends on channel --- + Inbox -> Receiver [label="inner", style=solid, color="#c62828", penwidth=1.5]; + Runtime -> Sender [label="transfer_txs", style=solid, color="#c62828", penwidth=1.5]; + + // --- runtime depends on actor --- + Inbox -> ActorAddress [label="addr", style=solid, color="#c62828", penwidth=1.5]; + Ctx -> ActorAddress [label="self_addr", style=solid, color="#c62828", penwidth=1.5]; + Runtime -> ActorAddress [label="spawn_txs", style=solid, color="#c62828", penwidth=1.5]; + Runtime -> AnyActor [label="spawn_txs", style=solid, color="#c62828", penwidth=1.5]; + Envelope -> ActorAddress [label="dest", style=solid, color="#c62828", penwidth=1.5]; + InboxRegistry -> ActorAddress [label="senders", style=solid, color="#c62828", penwidth=1.5]; + ContextInner -> ActorAddress [label="send_any", style=solid, color="#c62828", penwidth=1.5]; + ContextInner -> AnyActor [label="spawn_any", style=solid, color="#c62828", penwidth=1.5]; + + // --- runtime depends on address_map --- + Runtime -> AddressMap [label="address_map", style=solid, color="#c62828", penwidth=1.5]; + Runtime -> Placement [label="placement", style=solid, color="#c62828", penwidth=1.5]; + + // --- runtime depends on worker --- + Runtime -> Worker [label="single_worker", style=solid, color="#c62828", penwidth=1.5]; + + // --- worker depends on error --- + WorkerContext -> Error [label="send_any() param", style=solid, color="#e65100", penwidth=1.5]; + + // --- worker depends on config --- + TickContext -> RuntimeConfig [label="config", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> RuntimeConfig [label="config", style=solid, color="#e65100", penwidth=1.5]; + Worker -> BackoffPolicy [label="run() param", style=solid, color="#e65100", penwidth=1.5]; + + // --- worker depends on channel --- + TickContext -> Sender [label="transfer_txs", style=solid, color="#e65100", penwidth=1.5]; + Worker -> Receiver [label="transfer_rx", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> Sender [label="transfer_txs", style=solid, color="#e65100", penwidth=1.5]; + + // --- worker depends on actor --- + TickContext -> ActorAddress [label="spawn_txs", style=solid, color="#e65100", penwidth=1.5]; + TickContext -> AnyActor [label="spawn_txs", style=solid, color="#e65100", penwidth=1.5]; + Worker -> ActorAddress [label="spawn_rx", style=solid, color="#e65100", penwidth=1.5]; + Worker -> AnyActor [label="spawn_rx", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> ActorAddress [label="spawn_txs", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> AnyActor [label="spawn_txs", style=solid, color="#e65100", penwidth=1.5]; + ActorPool -> ActorAddress [label="actors", style=solid, color="#e65100", penwidth=1.5]; + ActorPool -> AnyActor [label="actors", style=solid, color="#e65100", penwidth=1.5]; + + // --- worker depends on address_map --- + TickContext -> AddressMap [label="address_map", style=solid, color="#e65100", penwidth=1.5]; + TickContext -> Placement [label="placement", style=solid, color="#e65100", penwidth=1.5]; + Worker -> WorkerId [label="id", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> WorkerId [label="worker_id", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> AddressMap [label="address_map", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> Placement [label="placement", style=solid, color="#e65100", penwidth=1.5]; + + // --- worker depends on runtime --- + TickContext -> Envelope [label="transfer_txs", style=solid, color="#e65100", penwidth=1.5]; + TickContext -> InboxRegistry [label="inbox_registry", style=solid, color="#e65100", penwidth=1.5]; + Worker -> Envelope [label="transfer_rx", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> Envelope [label="transfer_txs", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> InboxRegistry [label="inbox_registry", style=solid, color="#e65100", penwidth=1.5]; + WorkerContext -> ContextInner [label="impl", style=dotted, color="#e65100", penwidth=1.5]; + ActorPool -> ContextInner [label="tick_all() param", style=solid, color="#e65100", penwidth=1.5]; + + // --- python depends on actor --- + PyActorAddress -> ActorAddress [label="inner", style=solid, color="#999", penwidth=1.5]; + Effect -> ActorAddress [label="Send", style=solid, color="#999", penwidth=1.5]; + PyCtx -> ActorAddress [label="self_addr", style=solid, color="#999", penwidth=1.5]; + PyActor -> ActorInterface [label="impl", style=dotted, color="#999", penwidth=1.5]; + + // --- python depends on runtime --- + PyInbox -> Inbox [label="inner", style=solid, color="#999", penwidth=1.5]; + PyRuntime -> Runtime [label="inner", style=solid, color="#999", penwidth=1.5]; + PyRuntimeHandle -> RuntimeHandle [label="inner", style=solid, color="#999", penwidth=1.5]; + PyActor -> Ctx [label="handle() param", style=solid, color="#999", penwidth=1.5]; +} diff --git a/deps.html b/deps.html new file mode 100644 index 0000000..9268f46 --- /dev/null +++ b/deps.html @@ -0,0 +1,380 @@ + + + +swactor dependency DAG + + +
+ swactor dep graph   + + + + scroll to zoom · drag to pan · click node to focus +
+
+
Loading Graphviz…
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1e3b1de --- /dev/null +++ b/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "workspace", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@viz-js/viz": "^3.24.0" + } + }, + "node_modules/@viz-js/viz": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@viz-js/viz/-/viz-3.24.0.tgz", + "integrity": "sha512-sTRz2cFN6PwICVC7GVlF2aYNAZ/LwF7Y1mp3B+8LXQ7otyHrzQYSzNBwJ4lctL/aS3x97ppw59z6zIW+wPs6bQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5d39814 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@viz-js/viz": "^3.24.0" + } +} diff --git a/tools/depgraph/Cargo.lock b/tools/depgraph/Cargo.lock new file mode 100644 index 0000000..96a0c68 --- /dev/null +++ b/tools/depgraph/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "depgraph" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" diff --git a/tools/depgraph/Cargo.toml b/tools/depgraph/Cargo.toml new file mode 100644 index 0000000..cbb06df --- /dev/null +++ b/tools/depgraph/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "depgraph" +version = "0.1.0" +edition = "2021" + +[dependencies] +syn = { version = "2", features = ["full", "visit"] } +quote = "1" diff --git a/tools/depgraph/src/main.rs b/tools/depgraph/src/main.rs new file mode 100644 index 0000000..2662195 --- /dev/null +++ b/tools/depgraph/src/main.rs @@ -0,0 +1,1344 @@ +//! Deterministic dependency graph generator for the swactor crate. +//! +//! Parses all `.rs` source files using `syn`, extracts type definitions, +//! imports, and cross-module dependencies, then outputs `deps.dot` and +//! `deps.html` files. + +use std::collections::HashMap; +use std::fmt::Write as FmtWrite; +use std::fs; +use std::path::{Path, PathBuf}; + +// ─── Data structures ───────────────────────────────────────────────────────── + +#[derive(Debug, Clone, PartialEq, Eq)] +enum TypeKind { + Struct, + Trait, + Enum, +} + +#[derive(Debug, Clone)] +struct TypeInfo { + name: String, + kind: TypeKind, + fields: Vec<(String, String)>, // (field_name, type_description) +} + +#[derive(Debug)] +struct ModuleInfo { + name: String, + feature_gate: Option, + types: Vec, + /// local_name → (source_module, original_name) + imports: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[allow(dead_code)] +enum EdgeKind { + Field, + TraitImpl, + TraitObject, +} + +#[derive(Debug, Clone)] +struct Edge { + from_module: String, + from_type: String, + to_module: String, + to_type: String, + kind: EdgeKind, + label: String, +} + +// ─── Module colors ─────────────────────────────────────────────────────────── + +fn module_colors(module: &str) -> (&'static str, &'static str, &'static str) { + // Returns (cluster_fill, cluster_border, node_fill) + match module { + "error" => ("#f0f0f0", "#888", "#e8f5e9"), + "config" => ("#f0f0f0", "#888", "#e8f5e9"), + "channel" => ("#f0f0f0", "#888", "#fff9c4"), + "actor" => ("#e3f2fd", "#1565c0", "#bbdefb"), + "address_map" => ("#f3e5f5", "#7b1fa2", "#e1bee7"), + "runtime" => ("#fce4ec", "#c62828", "#ffcdd2"), + "worker" => ("#fff3e0", "#e65100", "#ffe0b2"), + "python" => ("#f5f5f5", "#999", "#d7ccc8"), + _ => ("#f0f0f0", "#888", "#e0e0e0"), + } +} + +fn module_edge_color(module: &str) -> &'static str { + match module { + "error" | "config" | "channel" => "#666", + "actor" => "#1565c0", + "address_map" => "#7b1fa2", + "runtime" => "#c62828", + "worker" => "#e65100", + "python" => "#999", + _ => "#666", + } +} + +// ─── Phase 1: Module discovery ─────────────────────────────────────────────── + +fn discover_modules(src_dir: &Path) -> Vec<(String, Option, PathBuf)> { + let lib_path = src_dir.join("lib.rs"); + let content = fs::read_to_string(&lib_path).expect("Failed to read src/lib.rs"); + let syntax = syn::parse_file(&content).expect("Failed to parse src/lib.rs"); + + let mut modules = Vec::new(); + let mut i = 0; + let items: Vec<&syn::Item> = syntax.items.iter().collect(); + + while i < items.len() { + // Check for #[cfg(feature = "...")] on the next item + let feature_gate = if let syn::Item::Mod(item_mod) = items[i] { + extract_feature_gate(&item_mod.attrs) + } else { + None + }; + + if let syn::Item::Mod(item_mod) = items[i] { + let mod_name = item_mod.ident.to_string(); + let mod_path = src_dir.join(format!("{}.rs", mod_name)); + if mod_path.exists() { + modules.push((mod_name, feature_gate, mod_path)); + } + } + + i += 1; + } + + modules +} + +fn extract_feature_gate(attrs: &[syn::Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("cfg") { + let tokens = attr.meta.require_list().ok()?.tokens.to_string(); + // Parse: feature = "python" + if let Some(pos) = tokens.find("feature") { + let rest = &tokens[pos..]; + if let Some(start) = rest.find('"') { + let rest = &rest[start + 1..]; + if let Some(end) = rest.find('"') { + return Some(rest[..end].to_string()); + } + } + } + } + } + None +} + +// ─── Phase 2: Parse & index ────────────────────────────────────────────────── + +fn parse_module(name: &str, path: &Path) -> ModuleInfo { + let content = fs::read_to_string(path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e)); + let syntax = syn::parse_file(&content) + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path.display(), e)); + + let mut types = Vec::new(); + + for item in &syntax.items { + match item { + syn::Item::Struct(s) => { + let fields = extract_struct_fields(s); + types.push(TypeInfo { + name: s.ident.to_string(), + kind: TypeKind::Struct, + fields, + }); + } + syn::Item::Trait(t) => { + let fields = extract_trait_items(t); + types.push(TypeInfo { + name: t.ident.to_string(), + kind: TypeKind::Trait, + fields, + }); + } + syn::Item::Enum(e) => { + let fields = extract_enum_variants(e); + types.push(TypeInfo { + name: e.ident.to_string(), + kind: TypeKind::Enum, + fields, + }); + } + _ => {} + } + } + + ModuleInfo { + name: name.to_string(), + feature_gate: None, // filled in later + types, + imports: HashMap::new(), // filled in phase 3 + } +} + +fn extract_struct_fields(s: &syn::ItemStruct) -> Vec<(String, String)> { + let mut fields = Vec::new(); + match &s.fields { + syn::Fields::Named(named) => { + for f in &named.named { + if let Some(ident) = &f.ident { + let ty = type_to_short_string(&f.ty); + fields.push((ident.to_string(), ty)); + } + } + } + syn::Fields::Unnamed(unnamed) => { + for (i, f) in unnamed.unnamed.iter().enumerate() { + let ty = type_to_short_string(&f.ty); + fields.push((format!("{}", i), ty)); + } + } + syn::Fields::Unit => {} + } + fields +} + +fn extract_trait_items(t: &syn::ItemTrait) -> Vec<(String, String)> { + let mut items = Vec::new(); + + // Extract associated types + for item in &t.items { + if let syn::TraitItem::Type(assoc) = item { + let bounds: Vec = assoc.bounds.iter().map(|b| quote_to_string(b)).collect(); + items.push((assoc.ident.to_string(), bounds.join(" + "))); + } + } + + // Extract method signatures (just name + simplified sig) + for item in &t.items { + if let syn::TraitItem::Fn(method) = item { + let sig = method_sig_short(&method.sig); + items.push((method.sig.ident.to_string(), sig)); + } + } + + items +} + +fn extract_enum_variants(e: &syn::ItemEnum) -> Vec<(String, String)> { + let mut variants = Vec::new(); + for v in &e.variants { + let fields_desc = match &v.fields { + syn::Fields::Named(named) => { + let parts: Vec = named + .named + .iter() + .filter_map(|f| { + f.ident + .as_ref() + .map(|id| format!("{}: {}", id, type_to_short_string(&f.ty))) + }) + .collect(); + format!("{{ {} }}", parts.join(", ")) + } + syn::Fields::Unnamed(unnamed) => { + let parts: Vec = unnamed + .unnamed + .iter() + .map(|f| type_to_short_string(&f.ty)) + .collect(); + format!("({})", parts.join(", ")) + } + syn::Fields::Unit => String::new(), + }; + variants.push((v.ident.to_string(), fields_desc)); + } + variants +} + +fn method_sig_short(sig: &syn::Signature) -> String { + let params: Vec = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => Some("&self".to_string()), + syn::FnArg::Typed(pat) => Some(type_to_short_string(&pat.ty)), + }) + .collect(); + let ret = match &sig.output { + syn::ReturnType::Default => String::new(), + syn::ReturnType::Type(_, ty) => format!(" → {}", type_to_short_string(ty)), + }; + format!("({}){}", params.join(", "), ret) +} + +fn type_to_short_string(ty: &syn::Type) -> String { + // Produce a compact but readable type representation + match ty { + syn::Type::Path(tp) => { + let segments: Vec = tp + .path + .segments + .iter() + .map(|seg| { + let name = seg.ident.to_string(); + match &seg.arguments { + syn::PathArguments::None => name, + syn::PathArguments::AngleBracketed(args) => { + let inner: Vec = args + .args + .iter() + .map(|a| match a { + syn::GenericArgument::Type(t) => type_to_short_string(t), + syn::GenericArgument::Lifetime(lt) => { + format!("'{}", lt.ident) + } + _ => quote_to_string(a), + }) + .collect(); + format!("{}<{}>", name, inner.join(", ")) + } + syn::PathArguments::Parenthesized(args) => { + let inner: Vec = + args.inputs.iter().map(type_to_short_string).collect(); + format!("{}({})", name, inner.join(", ")) + } + } + }) + .collect(); + segments.join("::") + } + syn::Type::Reference(r) => { + let lt = r + .lifetime + .as_ref() + .map(|l| format!("&'{} ", l.ident)) + .unwrap_or_else(|| "&".to_string()); + let mutability = if r.mutability.is_some() { "mut " } else { "" }; + format!("{}{}{}", lt, mutability, type_to_short_string(&r.elem)) + } + syn::Type::TraitObject(to) => { + let bounds: Vec = to.bounds.iter().map(|b| quote_to_string(b)).collect(); + format!("dyn {}", bounds.join(" + ")) + } + syn::Type::Tuple(t) => { + let inner: Vec = t.elems.iter().map(type_to_short_string).collect(); + format!("({})", inner.join(", ")) + } + syn::Type::Slice(s) => { + format!("[{}]", type_to_short_string(&s.elem)) + } + syn::Type::Array(a) => { + format!("[{}; ..]", type_to_short_string(&a.elem)) + } + _ => quote_to_string(ty), + } +} + +fn quote_to_string(t: &T) -> String { + t.to_token_stream().to_string() +} + +// ─── Phase 3: Import resolution ────────────────────────────────────────────── + +fn resolve_imports(modules: &mut [ModuleInfo], src_dir: &Path) { + // Build type_name → module_name lookup from all modules + let mut type_to_module: HashMap = HashMap::new(); + for module in modules.iter() { + for ty in &module.types { + type_to_module.insert(ty.name.clone(), module.name.clone()); + } + } + + // For each module, parse its use items and resolve imports + for module in modules.iter_mut() { + let path = src_dir.join(format!("{}.rs", module.name)); + let content = fs::read_to_string(&path).unwrap(); + let syntax = syn::parse_file(&content).unwrap(); + + for item in &syntax.items { + if let syn::Item::Use(use_item) = item { + collect_use_imports(&use_item.tree, &[], &mut module.imports); + } + } + } +} + +fn collect_use_imports( + tree: &syn::UseTree, + prefix: &[String], + imports: &mut HashMap, +) { + match tree { + syn::UseTree::Path(p) => { + let mut new_prefix = prefix.to_vec(); + new_prefix.push(p.ident.to_string()); + collect_use_imports(&p.tree, &new_prefix, imports); + } + syn::UseTree::Name(n) => { + let name = n.ident.to_string(); + if let Some(module) = extract_crate_module(prefix) { + imports.insert(name.clone(), (module, name)); + } + } + syn::UseTree::Rename(r) => { + let original = r.ident.to_string(); + let alias = r.rename.to_string(); + if let Some(module) = extract_crate_module(prefix) { + imports.insert(alias, (module, original)); + } + } + syn::UseTree::Glob(_) => { + // `use crate::foo::*` — we skip glob imports + } + syn::UseTree::Group(g) => { + for tree in &g.items { + collect_use_imports(tree, prefix, imports); + } + } + } +} + +/// Given a use path prefix like ["crate", "actor"], return the module name "actor". +/// Returns None for non-crate paths (std, external crates). +fn extract_crate_module(prefix: &[String]) -> Option { + if prefix.first().map(|s| s.as_str()) == Some("crate") { + prefix.get(1).cloned() + } else { + None + } +} + +// ─── Phase 4: Dependency extraction ────────────────────────────────────────── + +fn extract_edges(modules: &[ModuleInfo], src_dir: &Path) -> Vec { + let mut edges = Vec::new(); + + // Build type_name → module_name lookup + let mut type_to_module: HashMap = HashMap::new(); + for module in modules { + for ty in &module.types { + type_to_module.insert(ty.name.clone(), module.name.clone()); + } + } + + for module in modules { + // Parse file again for impl blocks + let path = src_dir.join(format!("{}.rs", module.name)); + let content = fs::read_to_string(&path).unwrap(); + let syntax = syn::parse_file(&content).unwrap(); + + // Extract edges from struct/trait/enum fields + for ty in &module.types { + for (_field_name, field_type) in &ty.fields { + let referenced = extract_type_names_from_string(field_type); + for ref_name in &referenced { + if ref_name == &ty.name { + continue; // skip self-references + } + if let Some(target_module) = resolve_type(ref_name, module, &type_to_module) { + edges.push(Edge { + from_module: module.name.clone(), + from_type: ty.name.clone(), + to_module: target_module.clone(), + to_type: ref_name.clone(), + kind: EdgeKind::Field, + label: _field_name.clone(), + }); + } + } + } + } + + // Extract edges from impl blocks + for item in &syntax.items { + if let syn::Item::Impl(impl_block) = item { + let self_type = extract_base_type_name(&impl_block.self_ty); + if self_type.is_none() { + continue; + } + let self_type = self_type.unwrap(); + let self_module = type_to_module.get(&self_type).cloned(); + + // Skip generic/blanket impls (self type is a type parameter, not a known type) + if self_module.is_none() { + continue; + } + + // Trait impl: `impl Trait for Type` + if let Some((_, trait_path, _)) = &impl_block.trait_ { + let trait_name = path_to_name(trait_path); + if trait_name == self_type { + // skip self-impl (e.g. blanket impls) + } else if is_std_type(&trait_name) { + // skip std trait impls (Send, Sync, Clone, etc.) + } else if let Some(target_module) = + resolve_type(&trait_name, module, &type_to_module) + { + // Attribute to the module where the self type lives + let from_mod = self_module.clone().unwrap_or(module.name.clone()); + edges.push(Edge { + from_module: from_mod, + from_type: self_type.clone(), + to_module: target_module, + to_type: trait_name.clone(), + kind: EdgeKind::TraitImpl, + label: "impl".to_string(), + }); + } + } + + // Only process method signatures for types belonging to this module + if self_module.as_deref() != Some(&module.name) { + continue; + } + + // Method signatures — extract types from params/return types + for impl_item in &impl_block.items { + if let syn::ImplItem::Fn(method) = impl_item { + let sig_types = extract_types_from_sig(&method.sig); + for ref_name in &sig_types { + if ref_name == &self_type { + continue; + } + if let Some(target_module) = + resolve_type(ref_name, module, &type_to_module) + { + let label = format!( + "{}() param", + method.sig.ident + ); + edges.push(Edge { + from_module: module.name.clone(), + from_type: self_type.clone(), + to_module: target_module, + to_type: ref_name.clone(), + kind: EdgeKind::Field, + label, + }); + } + } + } + } + } + } + + // Extract edges from trait definitions (method params referencing other types) + for item in &syntax.items { + if let syn::Item::Trait(trait_def) = item { + let trait_name = trait_def.ident.to_string(); + if type_to_module.get(&trait_name) != Some(&module.name) { + continue; + } + + for trait_item in &trait_def.items { + if let syn::TraitItem::Fn(method) = trait_item { + let sig_types = extract_types_from_sig(&method.sig); + for ref_name in &sig_types { + if ref_name == &trait_name { + continue; + } + if let Some(target_module) = + resolve_type(ref_name, module, &type_to_module) + { + let label = format!( + "{}() param", + method.sig.ident + ); + edges.push(Edge { + from_module: module.name.clone(), + from_type: trait_name.clone(), + to_module: target_module, + to_type: ref_name.clone(), + kind: EdgeKind::Field, + label, + }); + } + } + } + } + } + } + } + + // Deduplicate edges + dedup_edges(&mut edges); + edges +} + +fn dedup_edges(edges: &mut Vec) { + let mut seen = std::collections::HashSet::new(); + edges.retain(|e| { + let key = ( + e.from_module.clone(), + e.from_type.clone(), + e.to_module.clone(), + e.to_type.clone(), + e.kind.clone(), + ); + seen.insert(key) + }); +} + +/// Extract all type names referenced in a method signature +fn extract_types_from_sig(sig: &syn::Signature) -> Vec { + let mut types = Vec::new(); + for arg in &sig.inputs { + match arg { + syn::FnArg::Typed(pat_type) => { + collect_type_names(&pat_type.ty, &mut types); + } + _ => {} + } + } + if let syn::ReturnType::Type(_, ty) = &sig.output { + collect_type_names(ty, &mut types); + } + types +} + +/// Recursively collect type names from a syn::Type +fn collect_type_names(ty: &syn::Type, names: &mut Vec) { + match ty { + syn::Type::Path(tp) => { + for seg in &tp.path.segments { + let name = seg.ident.to_string(); + // Skip standard library / primitive wrappers + if !is_std_wrapper(&name) && !is_primitive(&name) { + names.push(name.clone()); + } + if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(inner) = arg { + collect_type_names(inner, names); + } + } + } + } + } + syn::Type::Reference(r) => { + collect_type_names(&r.elem, names); + } + syn::Type::TraitObject(to) => { + for bound in &to.bounds { + if let syn::TypeParamBound::Trait(t) = bound { + if let Some(seg) = t.path.segments.last() { + let name = seg.ident.to_string(); + if !is_std_type(&name) { + names.push(name); + } + } + } + } + } + syn::Type::Tuple(t) => { + for elem in &t.elems { + collect_type_names(elem, names); + } + } + syn::Type::Slice(s) => { + collect_type_names(&s.elem, names); + } + syn::Type::Paren(p) => { + collect_type_names(&p.elem, names); + } + _ => {} + } +} + +/// Given a short type name and a module's import map, resolve to the source module. +fn resolve_type( + name: &str, + module: &ModuleInfo, + type_to_module: &HashMap, +) -> Option { + // Check import map first + if let Some((src_module, _original)) = module.imports.get(name) { + // Verify the type actually exists in that module + if type_to_module.contains_key(name) { + return Some(src_module.clone()); + } + // The import pointed to a module, but the type name from the import + // might be the original name + if type_to_module.contains_key(_original) { + return Some(src_module.clone()); + } + } + + // Check if type is defined in any module + type_to_module.get(name).cloned() +} + +fn extract_base_type_name(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(tp) => { + tp.path.segments.last().map(|s| s.ident.to_string()) + } + _ => None, + } +} + +fn path_to_name(path: &syn::Path) -> String { + path.segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default() +} + +fn extract_type_names_from_string(type_str: &str) -> Vec { + // Extract PascalCase type names from a type string + let mut names = Vec::new(); + let mut current = String::new(); + + for ch in type_str.chars() { + if ch.is_alphanumeric() || ch == '_' { + current.push(ch); + } else { + if !current.is_empty() { + if is_pascal_case(¤t) + && !is_std_wrapper(¤t) + && !is_primitive(¤t) + && !is_std_type(¤t) + { + names.push(current.clone()); + } + current.clear(); + } + } + } + if !current.is_empty() + && is_pascal_case(¤t) + && !is_std_wrapper(¤t) + && !is_primitive(¤t) + && !is_std_type(¤t) + { + names.push(current); + } + + names +} + +fn is_pascal_case(s: &str) -> bool { + s.len() > 1 && s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) +} + +fn is_std_wrapper(name: &str) -> bool { + matches!( + name, + "Arc" | "Box" + | "Option" + | "Vec" + | "HashMap" + | "HashSet" + | "RwLock" + | "Mutex" + | "RefCell" + | "Cell" + | "Rc" + | "Result" + | "VecDeque" + | "BTreeMap" + | "BTreeSet" + | "AtomicBool" + | "AtomicUsize" + | "AtomicI64" + | "JoinHandle" + | "Ordering" + ) +} + +fn is_primitive(name: &str) -> bool { + matches!( + name, + "bool" | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "str" + | "String" + | "Self" + ) +} + +fn is_std_type(name: &str) -> bool { + matches!( + name, + "Any" | "Send" + | "Sync" + | "Sized" + | "Clone" + | "Copy" + | "Debug" + | "Display" + | "Default" + | "Hash" + | "Eq" + | "PartialEq" + | "Ord" + | "PartialOrd" + | "From" + | "Into" + | "AsRef" + | "Iterator" + | "IntoIterator" + | "ToString" + | "Hasher" + | "PyObject" + | "PyResult" + | "PyErr" + | "PyModule" + | "Python" + | "Bound" + | "PyAny" + | "ArrayQueue" + | "SegQueue" + ) +} + +// ─── Phase 5: DOT output ───────────────────────────────────────────────────── + +fn generate_dot(modules: &[ModuleInfo], edges: &[Edge]) -> String { + let mut out = String::new(); + + writeln!(out, "digraph swactor {{").unwrap(); + writeln!(out, " rankdir=LR;").unwrap(); + writeln!(out, " fontname=\"Helvetica\";").unwrap(); + writeln!(out, " fontsize=14;").unwrap(); + writeln!( + out, + " node [fontname=\"Helvetica\", fontsize=11, style=filled, shape=record];" + ) + .unwrap(); + writeln!(out, " edge [fontname=\"Helvetica\", fontsize=9];").unwrap(); + writeln!(out, " label=\"swactor — internal dependency DAG\";").unwrap(); + writeln!(out, " labelloc=t;").unwrap(); + writeln!(out, " compound=true;").unwrap(); + writeln!(out, " newrank=true;").unwrap(); + writeln!(out, " splines=ortho;").unwrap(); + writeln!(out).unwrap(); + + // Define module ordering for consistent output + let module_order = [ + "error", + "config", + "channel", + "actor", + "address_map", + "runtime", + "worker", + "python", + ]; + + // Emit subgraph clusters + for mod_name in &module_order { + if let Some(module) = modules.iter().find(|m| m.name == *mod_name) { + emit_cluster(&mut out, module); + } + } + + // Emit intra-module edges (within same cluster) + writeln!(out).unwrap(); + writeln!( + out, + " // ═══════════════════════════════════════════════════════════════════" + ) + .unwrap(); + writeln!( + out, + " // INTRA-MODULE EDGES (within same cluster)" + ) + .unwrap(); + writeln!( + out, + " // ═══════════════════════════════════════════════════════════════════" + ) + .unwrap(); + writeln!(out).unwrap(); + + for edge in edges.iter().filter(|e| e.from_module == e.to_module) { + emit_edge(&mut out, edge, true); + } + + // Emit cross-module edges + writeln!(out).unwrap(); + writeln!( + out, + " // ═══════════════════════════════════════════════════════════════════" + ) + .unwrap(); + writeln!( + out, + " // CROSS-MODULE EDGES (the real dependency DAG)" + ) + .unwrap(); + writeln!( + out, + " // ═══════════════════════════════════════════════════════════════════" + ) + .unwrap(); + + // Group cross-module edges by (from_module, to_module) + let mut grouped: HashMap<(String, String), Vec<&Edge>> = HashMap::new(); + for edge in edges.iter().filter(|e| e.from_module != e.to_module) { + grouped + .entry((edge.from_module.clone(), edge.to_module.clone())) + .or_default() + .push(edge); + } + + // Sort groups by module order for deterministic output + let mut group_keys: Vec<(String, String)> = grouped.keys().cloned().collect(); + group_keys.sort_by(|a, b| { + let ai = module_order + .iter() + .position(|m| *m == a.0) + .unwrap_or(99); + let bi = module_order + .iter() + .position(|m| *m == b.0) + .unwrap_or(99); + let aj = module_order + .iter() + .position(|m| *m == a.1) + .unwrap_or(99); + let bj = module_order + .iter() + .position(|m| *m == b.1) + .unwrap_or(99); + (ai, aj).cmp(&(bi, bj)) + }); + + for key in &group_keys { + let edges_group = &grouped[key]; + writeln!(out).unwrap(); + writeln!( + out, + " // --- {} depends on {} ---", + key.0, key.1 + ) + .unwrap(); + for edge in edges_group { + emit_edge(&mut out, edge, false); + } + } + + writeln!(out, "}}").unwrap(); + out +} + +fn emit_cluster(out: &mut String, module: &ModuleInfo) { + let (cluster_fill, cluster_border, node_fill) = module_colors(&module.name); + + let style = if module.feature_gate.is_some() { + "rounded,dashed,filled" + } else { + "rounded,filled" + }; + + let label = if module.feature_gate.is_some() { + format!("{} (feature-gated)", module.name) + } else { + module.name.clone() + }; + + writeln!( + out, + " subgraph cluster_{} {{", + module.name + ) + .unwrap(); + writeln!(out, " label=\"{}\";", label).unwrap(); + writeln!( + out, + " style=\"{}\"; fillcolor=\"{}\"; color=\"{}\";", + style, cluster_fill, cluster_border + ) + .unwrap(); + + for ty in &module.types { + let prefix = match ty.kind { + TypeKind::Trait => "«trait» ", + TypeKind::Enum => "«enum» ", + TypeKind::Struct => "", + }; + + let fields_str = if ty.fields.is_empty() { + String::new() + } else { + let field_lines: Vec = ty + .fields + .iter() + .map(|(name, ty_desc)| { + if ty_desc.is_empty() { + escape_dot(name) + } else if ty.kind == TypeKind::Trait { + // For traits, show method signatures + format!("{}({})", escape_dot(name), escape_dot(ty_desc)) + } else if ty.kind == TypeKind::Enum { + // For enum variants, show variant name and fields + if ty_desc.is_empty() { + escape_dot(name) + } else { + format!("{} {}", escape_dot(name), escape_dot(ty_desc)) + } + } else { + format!("{}: {}", escape_dot(name), escape_dot(ty_desc)) + } + }) + .collect(); + format!("|{}", field_lines.join("\\n")) + }; + + writeln!( + out, + " {} [label=\"{{{}{}{}}}\", fillcolor=\"{}\"];", + ty.name, prefix, ty.name, fields_str, node_fill + ) + .unwrap(); + } + + writeln!(out, " }}").unwrap(); +} + +fn emit_edge(out: &mut String, edge: &Edge, intra: bool) { + let color = if intra { + "#666" + } else { + module_edge_color(&edge.from_module) + }; + + let (style, penwidth) = match edge.kind { + EdgeKind::TraitImpl => { + if intra { + ("dotted", "1") + } else { + ("dotted", "1.5") + } + } + EdgeKind::TraitObject => { + if intra { + ("dashed", "1") + } else { + ("dashed", "1.5") + } + } + EdgeKind::Field => { + if intra { + ("dashed", "1") + } else { + ("solid", "1.5") + } + } + }; + + let label_escaped = escape_dot(&edge.label); + + writeln!( + out, + " {} -> {} [label=\"{}\", style={}, color=\"{}\", penwidth={}];", + edge.from_type, edge.to_type, label_escaped, style, color, penwidth + ) + .unwrap(); +} + +fn escape_dot(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('<', "\\<") + .replace('>', "\\>") + .replace('{', "\\{") + .replace('}', "\\}") + .replace('|', "\\|") +} + +// ─── Phase 6: HTML output ──────────────────────────────────────────────────── + +fn generate_html(dot_source: &str) -> String { + // Escape the DOT source for embedding in a JS template literal + let dot_escaped = dot_source + .replace('\\', "\\\\") + .replace('`', "\\`") + .replace("${", "\\${"); + + format!( + r##" + + +swactor dependency DAG + + +
+ swactor dep graph   + + + + scroll to zoom · drag to pan · click node to focus +
+
+
Loading Graphviz…
+ + +"## + ) +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +fn main() { + let args: Vec = std::env::args().collect(); + + let mut src_dir = PathBuf::from("src"); + let mut output_prefix = String::from("deps"); + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--src-dir" => { + i += 1; + src_dir = PathBuf::from(&args[i]); + } + "--output" => { + i += 1; + output_prefix = args[i].clone(); + } + "--help" | "-h" => { + eprintln!("Usage: depgraph [--src-dir src/] [--output deps]"); + eprintln!(" --src-dir DIR Source directory (default: src/)"); + eprintln!(" --output PREFIX Output prefix (default: deps)"); + eprintln!(" Produces PREFIX.dot and PREFIX.html"); + std::process::exit(0); + } + other => { + eprintln!("Unknown argument: {}", other); + std::process::exit(1); + } + } + i += 1; + } + + eprintln!("Scanning source directory: {}", src_dir.display()); + + // Phase 1: Module discovery + let module_defs = discover_modules(&src_dir); + eprintln!( + "Found {} modules: {}", + module_defs.len(), + module_defs + .iter() + .map(|(n, _, _)| n.as_str()) + .collect::>() + .join(", ") + ); + + // Phase 2: Parse & index + let mut modules: Vec = module_defs + .iter() + .map(|(name, feature, path)| { + let mut m = parse_module(name, path); + m.feature_gate = feature.clone(); + m + }) + .collect(); + + for m in &modules { + eprintln!( + " {} — {} types: {}", + m.name, + m.types.len(), + m.types + .iter() + .map(|t| t.name.as_str()) + .collect::>() + .join(", ") + ); + } + + // Phase 3: Import resolution + resolve_imports(&mut modules, &src_dir); + + // Phase 4: Dependency extraction + let edges = extract_edges(&modules, &src_dir); + eprintln!("Found {} dependency edges", edges.len()); + + let cross_module = edges + .iter() + .filter(|e| e.from_module != e.to_module) + .count(); + let intra_module = edges.len() - cross_module; + eprintln!( + " {} cross-module, {} intra-module", + cross_module, intra_module + ); + + // Phase 5: DOT output + let dot = generate_dot(&modules, &edges); + let dot_path = format!("{}.dot", output_prefix); + fs::write(&dot_path, &dot).expect("Failed to write .dot file"); + eprintln!("Wrote {}", dot_path); + + // Phase 6: HTML output + let html = generate_html(&dot); + let html_path = format!("{}.html", output_prefix); + fs::write(&html_path, &html).expect("Failed to write .html file"); + eprintln!("Wrote {}", html_path); + + eprintln!("Done!"); +}