Zoomable architecture diagrams of codebases, analyzed in your browser with Rust and WebAssembly
See the codeSee inside a codebase the way architecture figures do: as nested block diagrams you can zoom into without end. The repository is a diagram of programs and the files they exchange; zoom into a file and it becomes a diagram of its classes and functions; zoom into a function and you see its dataflow; zoom into a call and you are inside the code it calls.
Everything runs locally in the browser. A Rust core compiled to WebAssembly parses and lays out the whole project in a worker; the UI thread only draws.
FEATURES.md lists everything the app does.
npm install
npm run build # builds the wasm core, then the web app
npm run dev # http://127.0.0.1:5173 (run `npm run wasm` once first)
npm test # vitest + cargo test
Open a GitHub repository (#github=owner/repo in the URL works too), a local folder (picker or drag-and-drop), or paste a single file. The bundled example is karpathy/nanoGPT (MIT), which also serves as the main test fixture. tests/corpus.rs checks the abstractions on trimmed snapshots of a FastAPI app, httpx, nanochat, Flask, a Django site and HTTPie; tests/corpus_js.rs does the same for an Express API, a Next.js app, a NestJS service, a React/Vite app, ky, serve and a Turborepo workspace; tests/corpus_rust.rs for mini-redis, fd, ripgrep's core crate, a workspace of axum examples and llama2.rs; tests/corpus_go.rs for groupcache, a gin RealWorld API, redcon, chi, glow and golang/example. cargo test --test audit -- --ignored --nocapture reports the blocks most likely to read badly (code-like titles, guard tags, empty branch arms) across all four corpora.
A command-line view of the same analysis helps when tuning abstractions:
cargo run --release --bin blockviz -- path/to/repo --depth 4
Every level is a dataflow diagram read top to bottom: inputs enter at the top, results leave at the bottom, arrows show what flows into what.
| Level | Blocks | Arrows |
|---|---|---|
| Repository | folders, programs, files on disk | imports, exec, files written by one program and read by another |
| File | classes, functions, sections of a script, settings tables | which definition uses which; values between sections |
| Class | its sub-components (h: Block × n_layer) and methods | method calls |
| Function | calls into project code, sub-module calls, library operations, I/O | the values passed, labeled with variable names |
In nanoGPT this gives prepare.py → train.bin → train.py → ckpt.pt → sample.py at the top, LayerNorm, CausalSelfAttention, MLP → Block → GPT inside model.py, and x → ln_1 → attn → ⊕ → ln_2 → mlp → ⊕ inside Block.forward, the same structure as the Transformer figure, with each block zoomable into its own diagram.
A reader sees one small part at a time, so every part says where it fits.
#at=model.py/CausalSelfAttention/forward), so a link reopens it.… › Block › forward › attn), a line under the breadcrumbs says where that code is defined, and long breadcrumb paths fold their middle into a menu.Open a pull request (github.com/owner/repo/pull/123) or a comparison (…/compare/main...feature) and the diagram is the code after the change, drawn the same way, colored by what the change did: added blocks green, changed ones amber, what it did not touch faded, with new connections in green and removed ones as dashed red curves. Every block counts the changes inside it, so from the whole project down you see which parts of the architecture the change touches; the details panel lists the changes in diagram order (N/P step through them) and shows a block's code as it changed.
Both versions are analyzed (the head and the commit it branched off its base) and matched block by block: files by path through renames, folders by name or contents, blocks by kind and title, code moved within a file followed to its new place. Only files the change touched have changes, and a dependency still implied through others is neither new nor removed.
The goal is blocks a reader would name, not a syntax tree.
self.attn(x)), library operations and I/O. Glue (reshapes, builtins, arithmetic on sizes) is folded into the block it feeds, or into one block per statement.view · transpose), the developer's own name for a value it produces (token, allow_env_proxies), the field a store sets (Authorization, captioned set in request.headers). The caption says how, as the formula with long parts elided whole (settings.SENTRY_DSN and …). Code appears in a title only when it fits whole (T + 1).loss / steps. Each operand must be a single value from a different place; a + b + c is one ⊕ of three. A constant operand (T + 1), a longer formula or a store into a field stays a formula step.print, assert, imports, docstrings and super().__init__() never become blocks; side effects show as a marker and in the inspector.if __name__ == "__main__" is an entry-point marker plus a "Main" section, never a box containing __name__.× n_layer from ModuleList([... for _ in range(config.n_layer)])). A lone guarded statement gets an if … tag instead of a box.return None · if value is None, continue · if not line); every returned value reaches the function's result; if c: …; return x followed by more code is an if/else. No branch arm is drawn empty: one that only sets or returns a value shows that step.# model init, # training loop); runs of constants become a settings table.self.transformer.wte(idx) is an Embedding, block is a Block, model.configure_optimizers is GPT.configure_optimizers.@router.get("/{id}") on a router with prefix="/items" is the endpoint GET /items/{id}; click and Typer commands, Celery tasks and request hooks are labeled the same way, and Django urlpatterns become a route table.FastAPI(), Flask(), a WSGI app), routes, script, tests, config (Django settings, Sphinx conf.py), module. Entry points come from main guards, __main__.py, app objects, and anything a script, Dockerfile, CI workflow, Makefile, compose file or pyproject.toml/setup.cfg console script runs.speedrun.sh to see its steps in order, and into a step to see the program it runs.__all__ lists, type aliases, loggers and optional or type-checking imports are not blocks; constants gather into one table. Runs of statements on one subject are one block or table: calls (session · add → commit → refresh, which opens to each call in order), parser.add_argument × 12, fields set one after another (smtp_options · set × 3) and repeated text += ….__init__.py files are not drawn.attn_dropout is a dropout and headers is not a projection head.The same rules apply, read through JavaScript's idioms.
function f(), const f = () => …, exports.f = function …, class methods, getters and setters (tagged).this inside their functions is the object: a constructor function with prototype methods (function View() {}, View.prototype.render = …), an object with functions attached (var app = {}, app.listen = function …, or Object.create(proto)), and an object literal of methods. Calling into a module that exports such an object (require("./app").listen()) reaches its function. A wrapped function (catchAsync(async (req, res) => …), memo(Card)) is the function it wraps, tagged with the wrapper. A function that returns a function draws the returned one inside it.router.get("/:id", …) is the endpoint GET /v1/users/:id (mount prefixes from app.use("/v1/users", router) included), addEventListener("click", …) is click. Short arrows registered under a name count too.await does: fetch(url).then(r => r.json()).then(onData) is fetch → onData, with no block for the chain.each for map, forEach and filter; on click for an onClick handler, even inside a list.useX is a hook; elements are named by tag (<Dialog.Root>) and library elements given only literal props are decoration; "use client" modules are tagged. Next.js files get their roles (page, layout, route handler), NestJS controllers their @Controller prefixes, and Express apps their route tables.tsc do: tsconfig/jsconfig paths, baseUrl and extends, the @/ alias, workspace packages by name, .js specifiers that name .ts files, index files, require() and import(). Node builtins are not listed as packages, and stylesheet imports link to their files.PrismaClient.const users = new UsersService()) is known in every function of that module, including those written above it.switch, catch, this, objects and arrays.The same rules again, read through Rust's idioms.
impl blocks; an impl in another module is drawn where it is written and leads to its type, and its methods are found from calls anywhere. The traits a type implements are its subtitle, and each trait method says which trait it belongs to (fmt — Display, from — From<io::Error>). A trait lists its required methods and draws its provided ones.blocks: Block × n_layer, listener: TcpListener); wrappers are looked through (Arc<Mutex<Db>> is a Db), and standard types and synchronization primitives are state, not components. self.ln_1.forward(x) calls the component, as self.ln_1(x) does in PyTorch, so a candle or burn model reads like nanoGPT.if, match and blocks go where they are sent: let x = match … is a match whose arms set x, and a function's last expression is its result. Ok(x) and Some(x) are x; an Err handed back is an exit tagged returns error; ? passes values on. let … else is a guard (return · unless Some(path)).Mode::Search(mode) if threads == 1); names a pattern binds come from the matched value, and a value unpacked from an enum variant has the variant's type, so match self { Get(cmd) => cmd.apply(…) } reaches Get::apply. _ => {} arms are not drawn.format! and println! are formatted text (prints), vec![x; n] is [x] * n, assert! an assertion, panic!, bail! and unreachable! exits; logging macros are effects; a macro whose body is not Rust (json!) reads the variables it names.&x, .clone(), .unwrap(), .into(), .iter(), as) is left out, and xs.iter().map(|x| f(x)).collect() is a comprehension (each of xs).Cargo.toml (libraries, binaries, src/bin, examples, tests, benches, build scripts, workspaces), mod declarations with #[path], and crate::, super::, self:: and other crates of the workspace. A module depends on what its code uses: a crate root that only re-exports (pub use db::Db) is not drawn between its modules and their users..route("/users", get(list).post(create)) with .nest and .merge, actix-web's web::resource, web::scope and #[get("/…")] (and Rocket's attributes). Handlers are named by their routes and routers are route tables. Derives say what a type is for (command line for clap, DB row, error type), sqlx, diesel and sea-orm queries are database steps, and #[cfg(unix)], #[cfg(feature = "x")] variants are tagged with their condition.The same rules again, read through Go's idioms.
store.Open is a dependency on store/db.go, not on the package). Imports resolve through go.mod modules (nested modules, local replace directives, go.work), and by directory for code without a module.methods and lead to it. Embedded types are bases, so their methods are found. Interfaces are tables of method signatures, and a type lists the project's interfaces it satisfies (Circle — Shape), matched by method signature since Go never says so.return &pgStore{db} from a function returning Store). One such implementation and the call is that method (Get — pgStore); several and the call names them and leads to the interface (Lookup — lru or redisCache). A type that only happens to have the methods is not taken for it (a library's Validator is whatever its user registers), test doubles do not count outside tests, and methods that only return a field (Width()) read as the value.if err != nil { return …, err } (wrapped or not, with cleanup before it, or panic, log.Fatal, os.Exit after printing) leaves only the call, as Rust's ? does. Checks that do something else (retry, fall back, log and continue) stay branches. An error made here (return nil, ErrClosed, errors.New("…"), fmt.Errorf("…: %w", err)) is an exit tagged returns error, named by its message.go f() and defer f() are steps marked in background and on return; deferred cleanup (defer f.Close(), defer mu.Unlock(), a deferred function that only closes) and lock and WaitGroup bookkeeping are left out. Receiving (<-ch) waits for a value; a send is a send step. select is a choice among channel operations, a type switch a match whose cases type the value, and a switch without a tag an if/else if chain (fallthrough joins cases).db, err := Open(…) types db from Open's declared result, so db.Save(…) reaches the method. xs = append(xs, x) adds to a list, composite literals are constructor calls, lists and maps, conversions and &/* are glue, and fmt.Sprintf builds formatted text.r.GET), chi and fiber (r.Get, r.Route, r.With(…), r.Mount), gorilla and net/http (HandleFunc, Go 1.22 patterns like "GET /items/{id}"), with group prefixes followed across functions and packages (articles.Register(v1.Group("/articles"))). Handlers are named by their routes and registrations are route tables. cobra and urfave/cli commands are named by their Use; database/sql, sqlx, gorm, pgx, Mongo, Redis and bbolt calls are database steps (a gorm chain is named by the call that runs it); struct tags say what a type is for (serializable, DB row).package main with func main), tests (TestX, BenchmarkX, FuzzX, ExampleX are tagged; if got != want { t.Errorf(…) } is an assertion), or modules; build constraints (//go:build, // +build, _linux.go) tag the file, generated files (// Code generated … DO NOT EDIT.) are shown but not opened, and testdata and vendor are not the project's code.Every text file is handled. Notebooks are analyzed as Python (cells become sections titled by the markdown above them). Other source files get an outline: their definitions, calls between definitions in brace languages, and imports resolved to files for C/C++. Config files become key/value tables (.env values are masked), docs become heading lists, and READMEs describe their folder. Binary files appear as assets.
crates/blockviz-core/ Rust → WebAssembly
src/ir.rs the scene: one tree of nested, laid-out diagrams
src/naming.rs block titles from a language-neutral description of each step
src/vocabulary.rs what names and libraries mean: domains, NN categories, metadata,
database calls
src/code/syntax.rs the language-neutral syntax tree every front end lowers into
src/code/ analysis over it: source + comments, symbol index and type
inference, flow builder, effects
src/code/names.rs expressions described for naming; structural shortening
src/code/frameworks.rs routes, commands, tasks, components from decorators and calls
src/python/ Ruff's tree → syntax tree
src/js/ oxc's tree → syntax tree; import resolution (tsconfig, workspaces)
src/rust/ syn's tree → syntax tree; module resolution (Cargo manifests,
`mod` declarations); a small lexer for comments
src/go/ a Go parser (a port of go/scanner and go/parser, checked against
go/parser), its tree → syntax tree; packages by go.mod
src/invocations.rs commands in scripts and manifests that run project code
src/compose.rs Docker Compose services
src/outline.rs outlines and imports for other languages
src/highlight/ syntax classes for the source view: Ruff's tokens and tree for
Python, table-driven lexers for other languages and formats
src/project.rs repository tree, cross-file relations, artifacts, edge lifting
src/layout/ bottom-up sizing; layered layout with shared trunks, PAV placement,
orthogonal routing with track allocation
src/
core/ worker, analyzer, loaders (GitHub repositories, pull requests and
comparisons, folder, drop, paste), saved downloads and analyses
canvas/ camera (anchored for unlimited depth), renderer, engine, locations
(paths as titles), value tracing, scene diff (two versions matched
block by block)
ui/ inspector (resizable, source linked to blocks, used by), value panel,
changes panel and line diff, history and links, breadcrumbs,
search, open dialog
One uniform scene model. Every node lives in its parent's interior coordinates. A node with an interior (children, a table, or a ref to another node's interior) draws it inside its inner rectangle, scaled to fit. That single rule is what makes zoom unbounded: the renderer never needs to know what a node is to descend into it. A call borrows its callee's interior by reference, so the same definition appears wherever it is called without being duplicated.
Layout happens once, in Rust, bottom-up: an interior is laid out before the box that contains it is sized. Edges carrying the same value share a trunk so one line visibly splits into its consumers, labeled under the block it leaves; with "Tidy arrows" (the switch beside the home button, or --trunks on the command line) all values a block sends share one trunk that branches to each reader, every branch labeled just above the block it reaches. Node positions are solved per layer as weighted isotonic regression, which keeps long edges straight.
Rendering is immediate-mode Canvas 2D, driven only by invalidation. Each frame walks what is visible; boxes open smoothly as their interior becomes large enough to show something; text fades in by on-screen size. The camera is anchored to the deepest container that fills the viewport and every transform is derived from it, so precision does not degrade however deep you zoom.
Python parsing uses Ruff's parser (ruff_python_parser), which follows the current grammar: PEP 701 f-strings and PEP 695 type parameters (3.12), type parameter defaults (3.13), t-strings and unparenthesized except A, B: (3.14), and lazy import, unpacking in comprehensions and case +0: (3.15). tests/python_syntax.rs covers each release from 3.8 to 3.15. The whole CPython 3.15.0rc3 Lib/ (2,052 files, test suite included) parses with no errors apart from the one file CPython keeps deliberately invalid. The parser recovers from syntax errors, so a file with a typo still renders everything around the error.
JavaScript and TypeScript are parsed by oxc (oxc_parser): ES2025 (import attributes, using declarations, regexp v flag, top-level await), current TypeScript (decorators, accessor, satisfies, const type parameters, asserts guards) and JSX in .jsx/.tsx. tests/js_syntax.rs covers each, plus CommonJS and UMD modules. Where oxc gives up at a syntax error, the statement holding it is blanked out and the file parsed again, so the rest still renders. vuejs/core (about 160k lines of TypeScript) analyzes in under a second natively.
Rust is parsed by syn with span locations: editions 2015 to 2024, including let … else, if let chains, async closures, use<…> capture lists, &raw references, inline const blocks, C string literals and unsafe extern blocks. tests/rust_syntax.rs covers each on a small crate with every kind of target. syn does not recover from syntax errors, so on an error the body of the function holding it (or the top-level item) is blanked out and the file parsed again. ripgrep (about 56k lines) analyzes in under half a second natively.
Go is parsed by a hand-written port of Go's own scanner and parser (src/go/), up to Go 1.24: generics with type sets and generic type aliases, range over integers and functions, and every older form. Its tree mirrors go/ast node for node, positions included, and tests/go_parser.rs compares it with go/parser's: committed golden dumps in CI, and, with Go installed, every file of the Go distribution (cargo test --release --test go_parser go_distribution -- --ignored), where all 7,804 files go/parser accepts give identical trees. Like go/parser it recovers from syntax errors (a broken statement or declaration is skipped and parsing resumes), never panics on damaged input and caps nesting so deep input cannot overflow the stack. The standard library (2M lines) parses in about 0.65 s natively; tests/go_syntax.rs covers the syntax of each release on a small module. prometheus (about 390k lines, with its vendored JavaScript) analyzes in about 3.4 s natively.
This is static analysis: arrows show likely dataflow and dependencies, not a runtime trace. A call through a contract (an abstract method, a TypeScript interface, a Rust dyn Trait, a Go interface) goes to the implementation when the project has just one, and names the candidates when it has several; a method overridden without being abstract is still the base class's. Code passed through strings and monkey-patching are invisible. TypeScript types are read only as far as naming a class; longer types are shown elided by their brackets ({ [K in keyof T as …]: () => T[K] }). A generic parameter is the trait or interface it is bounded by (M: Matcher, [S Store], <R extends Repo>), but generic types are not instantiated (a field typed by a parameter of another type stays unknown), and macros are read by their name and arguments, not expanded. Go calls through interface values are resolved only as far as the program shows which types go in (not through values a library hands back), and what a library function returns is known only for a few (gin.Default()), so a method called on such a value is a plain step. A branch with more than 24 arms (an option parser's switch arg) is drawn as a table of its arms. Very large repositories work but get heavy in the browser (the full CPython standard library, about 330k lines, produces a 64 MB scene).
MIT. The open-source projects copied into crates/blockviz-core/tests/fixtures/ for tests (nanoGPT and those under corpus*/) keep their own licenses.
Zoomable architecture diagrams of codebases, analyzed in your browser with Rust and WebAssembly
See the codeSee inside a codebase the way architecture figures do: as nested block diagrams you can zoom into without end. The repository is a diagram of programs and the files they exchange; zoom into a file and it becomes a diagram of its classes and functions; zoom into a function and you see its dataflow; zoom into a call and you are inside the code it calls.
Everything runs locally in the browser. A Rust core compiled to WebAssembly parses and lays out the whole project in a worker; the UI thread only draws.
FEATURES.md lists everything the app does.
npm install
npm run build # builds the wasm core, then the web app
npm run dev # http://127.0.0.1:5173 (run `npm run wasm` once first)
npm test # vitest + cargo test
Open a GitHub repository (#github=owner/repo in the URL works too), a local folder (picker or drag-and-drop), or paste a single file. The bundled example is karpathy/nanoGPT (MIT), which also serves as the main test fixture. tests/corpus.rs checks the abstractions on trimmed snapshots of a FastAPI app, httpx, nanochat, Flask, a Django site and HTTPie; tests/corpus_js.rs does the same for an Express API, a Next.js app, a NestJS service, a React/Vite app, ky, serve and a Turborepo workspace; tests/corpus_rust.rs for mini-redis, fd, ripgrep's core crate, a workspace of axum examples and llama2.rs; tests/corpus_go.rs for groupcache, a gin RealWorld API, redcon, chi, glow and golang/example. cargo test --test audit -- --ignored --nocapture reports the blocks most likely to read badly (code-like titles, guard tags, empty branch arms) across all four corpora.
A command-line view of the same analysis helps when tuning abstractions:
cargo run --release --bin blockviz -- path/to/repo --depth 4
Every level is a dataflow diagram read top to bottom: inputs enter at the top, results leave at the bottom, arrows show what flows into what.
| Level | Blocks | Arrows |
|---|---|---|
| Repository | folders, programs, files on disk | imports, exec, files written by one program and read by another |
| File | classes, functions, sections of a script, settings tables | which definition uses which; values between sections |
| Class | its sub-components (h: Block × n_layer) and methods | method calls |
| Function | calls into project code, sub-module calls, library operations, I/O | the values passed, labeled with variable names |
In nanoGPT this gives prepare.py → train.bin → train.py → ckpt.pt → sample.py at the top, LayerNorm, CausalSelfAttention, MLP → Block → GPT inside model.py, and x → ln_1 → attn → ⊕ → ln_2 → mlp → ⊕ inside Block.forward, the same structure as the Transformer figure, with each block zoomable into its own diagram.
A reader sees one small part at a time, so every part says where it fits.
#at=model.py/CausalSelfAttention/forward), so a link reopens it.… › Block › forward › attn), a line under the breadcrumbs says where that code is defined, and long breadcrumb paths fold their middle into a menu.Open a pull request (github.com/owner/repo/pull/123) or a comparison (…/compare/main...feature) and the diagram is the code after the change, drawn the same way, colored by what the change did: added blocks green, changed ones amber, what it did not touch faded, with new connections in green and removed ones as dashed red curves. Every block counts the changes inside it, so from the whole project down you see which parts of the architecture the change touches; the details panel lists the changes in diagram order (N/P step through them) and shows a block's code as it changed.
Both versions are analyzed (the head and the commit it branched off its base) and matched block by block: files by path through renames, folders by name or contents, blocks by kind and title, code moved within a file followed to its new place. Only files the change touched have changes, and a dependency still implied through others is neither new nor removed.
The goal is blocks a reader would name, not a syntax tree.
self.attn(x)), library operations and I/O. Glue (reshapes, builtins, arithmetic on sizes) is folded into the block it feeds, or into one block per statement.view · transpose), the developer's own name for a value it produces (token, allow_env_proxies), the field a store sets (Authorization, captioned set in request.headers). The caption says how, as the formula with long parts elided whole (settings.SENTRY_DSN and …). Code appears in a title only when it fits whole (T + 1).loss / steps. Each operand must be a single value from a different place; a + b + c is one ⊕ of three. A constant operand (T + 1), a longer formula or a store into a field stays a formula step.print, assert, imports, docstrings and super().__init__() never become blocks; side effects show as a marker and in the inspector.if __name__ == "__main__" is an entry-point marker plus a "Main" section, never a box containing __name__.× n_layer from ModuleList([... for _ in range(config.n_layer)])). A lone guarded statement gets an if … tag instead of a box.return None · if value is None, continue · if not line); every returned value reaches the function's result; if c: …; return x followed by more code is an if/else. No branch arm is drawn empty: one that only sets or returns a value shows that step.# model init, # training loop); runs of constants become a settings table.self.transformer.wte(idx) is an Embedding, block is a Block, model.configure_optimizers is GPT.configure_optimizers.@router.get("/{id}") on a router with prefix="/items" is the endpoint GET /items/{id}; click and Typer commands, Celery tasks and request hooks are labeled the same way, and Django urlpatterns become a route table.FastAPI(), Flask(), a WSGI app), routes, script, tests, config (Django settings, Sphinx conf.py), module. Entry points come from main guards, __main__.py, app objects, and anything a script, Dockerfile, CI workflow, Makefile, compose file or pyproject.toml/setup.cfg console script runs.speedrun.sh to see its steps in order, and into a step to see the program it runs.__all__ lists, type aliases, loggers and optional or type-checking imports are not blocks; constants gather into one table. Runs of statements on one subject are one block or table: calls (session · add → commit → refresh, which opens to each call in order), parser.add_argument × 12, fields set one after another (smtp_options · set × 3) and repeated text += ….__init__.py files are not drawn.attn_dropout is a dropout and headers is not a projection head.The same rules apply, read through JavaScript's idioms.
function f(), const f = () => …, exports.f = function …, class methods, getters and setters (tagged).this inside their functions is the object: a constructor function with prototype methods (function View() {}, View.prototype.render = …), an object with functions attached (var app = {}, app.listen = function …, or Object.create(proto)), and an object literal of methods. Calling into a module that exports such an object (require("./app").listen()) reaches its function. A wrapped function (catchAsync(async (req, res) => …), memo(Card)) is the function it wraps, tagged with the wrapper. A function that returns a function draws the returned one inside it.router.get("/:id", …) is the endpoint GET /v1/users/:id (mount prefixes from app.use("/v1/users", router) included), addEventListener("click", …) is click. Short arrows registered under a name count too.await does: fetch(url).then(r => r.json()).then(onData) is fetch → onData, with no block for the chain.each for map, forEach and filter; on click for an onClick handler, even inside a list.useX is a hook; elements are named by tag (<Dialog.Root>) and library elements given only literal props are decoration; "use client" modules are tagged. Next.js files get their roles (page, layout, route handler), NestJS controllers their @Controller prefixes, and Express apps their route tables.tsc do: tsconfig/jsconfig paths, baseUrl and extends, the @/ alias, workspace packages by name, .js specifiers that name .ts files, index files, require() and import(). Node builtins are not listed as packages, and stylesheet imports link to their files.PrismaClient.const users = new UsersService()) is known in every function of that module, including those written above it.switch, catch, this, objects and arrays.The same rules again, read through Rust's idioms.
impl blocks; an impl in another module is drawn where it is written and leads to its type, and its methods are found from calls anywhere. The traits a type implements are its subtitle, and each trait method says which trait it belongs to (fmt — Display, from — From<io::Error>). A trait lists its required methods and draws its provided ones.blocks: Block × n_layer, listener: TcpListener); wrappers are looked through (Arc<Mutex<Db>> is a Db), and standard types and synchronization primitives are state, not components. self.ln_1.forward(x) calls the component, as self.ln_1(x) does in PyTorch, so a candle or burn model reads like nanoGPT.if, match and blocks go where they are sent: let x = match … is a match whose arms set x, and a function's last expression is its result. Ok(x) and Some(x) are x; an Err handed back is an exit tagged returns error; ? passes values on. let … else is a guard (return · unless Some(path)).Mode::Search(mode) if threads == 1); names a pattern binds come from the matched value, and a value unpacked from an enum variant has the variant's type, so match self { Get(cmd) => cmd.apply(…) } reaches Get::apply. _ => {} arms are not drawn.format! and println! are formatted text (prints), vec![x; n] is [x] * n, assert! an assertion, panic!, bail! and unreachable! exits; logging macros are effects; a macro whose body is not Rust (json!) reads the variables it names.&x, .clone(), .unwrap(), .into(), .iter(), as) is left out, and xs.iter().map(|x| f(x)).collect() is a comprehension (each of xs).Cargo.toml (libraries, binaries, src/bin, examples, tests, benches, build scripts, workspaces), mod declarations with #[path], and crate::, super::, self:: and other crates of the workspace. A module depends on what its code uses: a crate root that only re-exports (pub use db::Db) is not drawn between its modules and their users..route("/users", get(list).post(create)) with .nest and .merge, actix-web's web::resource, web::scope and #[get("/…")] (and Rocket's attributes). Handlers are named by their routes and routers are route tables. Derives say what a type is for (command line for clap, DB row, error type), sqlx, diesel and sea-orm queries are database steps, and #[cfg(unix)], #[cfg(feature = "x")] variants are tagged with their condition.The same rules again, read through Go's idioms.
store.Open is a dependency on store/db.go, not on the package). Imports resolve through go.mod modules (nested modules, local replace directives, go.work), and by directory for code without a module.methods and lead to it. Embedded types are bases, so their methods are found. Interfaces are tables of method signatures, and a type lists the project's interfaces it satisfies (Circle — Shape), matched by method signature since Go never says so.return &pgStore{db} from a function returning Store). One such implementation and the call is that method (Get — pgStore); several and the call names them and leads to the interface (Lookup — lru or redisCache). A type that only happens to have the methods is not taken for it (a library's Validator is whatever its user registers), test doubles do not count outside tests, and methods that only return a field (Width()) read as the value.if err != nil { return …, err } (wrapped or not, with cleanup before it, or panic, log.Fatal, os.Exit after printing) leaves only the call, as Rust's ? does. Checks that do something else (retry, fall back, log and continue) stay branches. An error made here (return nil, ErrClosed, errors.New("…"), fmt.Errorf("…: %w", err)) is an exit tagged returns error, named by its message.go f() and defer f() are steps marked in background and on return; deferred cleanup (defer f.Close(), defer mu.Unlock(), a deferred function that only closes) and lock and WaitGroup bookkeeping are left out. Receiving (<-ch) waits for a value; a send is a send step. select is a choice among channel operations, a type switch a match whose cases type the value, and a switch without a tag an if/else if chain (fallthrough joins cases).db, err := Open(…) types db from Open's declared result, so db.Save(…) reaches the method. xs = append(xs, x) adds to a list, composite literals are constructor calls, lists and maps, conversions and &/* are glue, and fmt.Sprintf builds formatted text.r.GET), chi and fiber (r.Get, r.Route, r.With(…), r.Mount), gorilla and net/http (HandleFunc, Go 1.22 patterns like "GET /items/{id}"), with group prefixes followed across functions and packages (articles.Register(v1.Group("/articles"))). Handlers are named by their routes and registrations are route tables. cobra and urfave/cli commands are named by their Use; database/sql, sqlx, gorm, pgx, Mongo, Redis and bbolt calls are database steps (a gorm chain is named by the call that runs it); struct tags say what a type is for (serializable, DB row).package main with func main), tests (TestX, BenchmarkX, FuzzX, ExampleX are tagged; if got != want { t.Errorf(…) } is an assertion), or modules; build constraints (//go:build, // +build, _linux.go) tag the file, generated files (// Code generated … DO NOT EDIT.) are shown but not opened, and testdata and vendor are not the project's code.Every text file is handled. Notebooks are analyzed as Python (cells become sections titled by the markdown above them). Other source files get an outline: their definitions, calls between definitions in brace languages, and imports resolved to files for C/C++. Config files become key/value tables (.env values are masked), docs become heading lists, and READMEs describe their folder. Binary files appear as assets.
crates/blockviz-core/ Rust → WebAssembly
src/ir.rs the scene: one tree of nested, laid-out diagrams
src/naming.rs block titles from a language-neutral description of each step
src/vocabulary.rs what names and libraries mean: domains, NN categories, metadata,
database calls
src/code/syntax.rs the language-neutral syntax tree every front end lowers into
src/code/ analysis over it: source + comments, symbol index and type
inference, flow builder, effects
src/code/names.rs expressions described for naming; structural shortening
src/code/frameworks.rs routes, commands, tasks, components from decorators and calls
src/python/ Ruff's tree → syntax tree
src/js/ oxc's tree → syntax tree; import resolution (tsconfig, workspaces)
src/rust/ syn's tree → syntax tree; module resolution (Cargo manifests,
`mod` declarations); a small lexer for comments
src/go/ a Go parser (a port of go/scanner and go/parser, checked against
go/parser), its tree → syntax tree; packages by go.mod
src/invocations.rs commands in scripts and manifests that run project code
src/compose.rs Docker Compose services
src/outline.rs outlines and imports for other languages
src/highlight/ syntax classes for the source view: Ruff's tokens and tree for
Python, table-driven lexers for other languages and formats
src/project.rs repository tree, cross-file relations, artifacts, edge lifting
src/layout/ bottom-up sizing; layered layout with shared trunks, PAV placement,
orthogonal routing with track allocation
src/
core/ worker, analyzer, loaders (GitHub repositories, pull requests and
comparisons, folder, drop, paste), saved downloads and analyses
canvas/ camera (anchored for unlimited depth), renderer, engine, locations
(paths as titles), value tracing, scene diff (two versions matched
block by block)
ui/ inspector (resizable, source linked to blocks, used by), value panel,
changes panel and line diff, history and links, breadcrumbs,
search, open dialog
One uniform scene model. Every node lives in its parent's interior coordinates. A node with an interior (children, a table, or a ref to another node's interior) draws it inside its inner rectangle, scaled to fit. That single rule is what makes zoom unbounded: the renderer never needs to know what a node is to descend into it. A call borrows its callee's interior by reference, so the same definition appears wherever it is called without being duplicated.
Layout happens once, in Rust, bottom-up: an interior is laid out before the box that contains it is sized. Edges carrying the same value share a trunk so one line visibly splits into its consumers, labeled under the block it leaves; with "Tidy arrows" (the switch beside the home button, or --trunks on the command line) all values a block sends share one trunk that branches to each reader, every branch labeled just above the block it reaches. Node positions are solved per layer as weighted isotonic regression, which keeps long edges straight.
Rendering is immediate-mode Canvas 2D, driven only by invalidation. Each frame walks what is visible; boxes open smoothly as their interior becomes large enough to show something; text fades in by on-screen size. The camera is anchored to the deepest container that fills the viewport and every transform is derived from it, so precision does not degrade however deep you zoom.
Python parsing uses Ruff's parser (ruff_python_parser), which follows the current grammar: PEP 701 f-strings and PEP 695 type parameters (3.12), type parameter defaults (3.13), t-strings and unparenthesized except A, B: (3.14), and lazy import, unpacking in comprehensions and case +0: (3.15). tests/python_syntax.rs covers each release from 3.8 to 3.15. The whole CPython 3.15.0rc3 Lib/ (2,052 files, test suite included) parses with no errors apart from the one file CPython keeps deliberately invalid. The parser recovers from syntax errors, so a file with a typo still renders everything around the error.
JavaScript and TypeScript are parsed by oxc (oxc_parser): ES2025 (import attributes, using declarations, regexp v flag, top-level await), current TypeScript (decorators, accessor, satisfies, const type parameters, asserts guards) and JSX in .jsx/.tsx. tests/js_syntax.rs covers each, plus CommonJS and UMD modules. Where oxc gives up at a syntax error, the statement holding it is blanked out and the file parsed again, so the rest still renders. vuejs/core (about 160k lines of TypeScript) analyzes in under a second natively.
Rust is parsed by syn with span locations: editions 2015 to 2024, including let … else, if let chains, async closures, use<…> capture lists, &raw references, inline const blocks, C string literals and unsafe extern blocks. tests/rust_syntax.rs covers each on a small crate with every kind of target. syn does not recover from syntax errors, so on an error the body of the function holding it (or the top-level item) is blanked out and the file parsed again. ripgrep (about 56k lines) analyzes in under half a second natively.
Go is parsed by a hand-written port of Go's own scanner and parser (src/go/), up to Go 1.24: generics with type sets and generic type aliases, range over integers and functions, and every older form. Its tree mirrors go/ast node for node, positions included, and tests/go_parser.rs compares it with go/parser's: committed golden dumps in CI, and, with Go installed, every file of the Go distribution (cargo test --release --test go_parser go_distribution -- --ignored), where all 7,804 files go/parser accepts give identical trees. Like go/parser it recovers from syntax errors (a broken statement or declaration is skipped and parsing resumes), never panics on damaged input and caps nesting so deep input cannot overflow the stack. The standard library (2M lines) parses in about 0.65 s natively; tests/go_syntax.rs covers the syntax of each release on a small module. prometheus (about 390k lines, with its vendored JavaScript) analyzes in about 3.4 s natively.
This is static analysis: arrows show likely dataflow and dependencies, not a runtime trace. A call through a contract (an abstract method, a TypeScript interface, a Rust dyn Trait, a Go interface) goes to the implementation when the project has just one, and names the candidates when it has several; a method overridden without being abstract is still the base class's. Code passed through strings and monkey-patching are invisible. TypeScript types are read only as far as naming a class; longer types are shown elided by their brackets ({ [K in keyof T as …]: () => T[K] }). A generic parameter is the trait or interface it is bounded by (M: Matcher, [S Store], <R extends Repo>), but generic types are not instantiated (a field typed by a parameter of another type stays unknown), and macros are read by their name and arguments, not expanded. Go calls through interface values are resolved only as far as the program shows which types go in (not through values a library hands back), and what a library function returns is known only for a few (gin.Default()), so a method called on such a value is a plain step. A branch with more than 24 arms (an option parser's switch arg) is drawn as a table of its arms. Very large repositories work but get heavy in the browser (the full CPython standard library, about 330k lines, produces a 64 MB scene).
MIT. The open-source projects copied into crates/blockviz-core/tests/fixtures/ for tests (nanoGPT and those under corpus*/) keep their own licenses.