Mirage analyzes control-flow graphs from Magellan databases. It enumerates execution paths, computes dominator trees, detects loops, finds dead code, identifies cycles, scores risk, and suggests refactorings – all without being a compiler pass or a language server. It reads what Magellan indexed and answers questions about program structure.

Mirage is intentionally downstream of Magellan: it does not own indexing, parsing, or editing. It consumes indexed CFG data and turns it into control-flow answers.

The repo is at github.com/oldnordic/mirage. 31.4K lines of Rust, 354 commits, 474 tests. This is the development timeline.


Starting from scratch (February 1)

Day one was research. I evaluated two approaches for getting CFG data:

  1. MIR extraction via Charon – Use the Charon tool to extract Rust’s Mid-Level IR (ULLBC format), then convert to a CFG. This would give compiler-quality data but required maintaining a separate toolchain dependency.
  2. AST-based CFG construction – Parse source with tree-sitter, build CFGs from syntax. Faster to implement but less precise.

I built both. The MIR path produced data structures and conversion logic but ultimately returned empty results – Charon couldn’t handle the real codebases in my stack. The AST path worked immediately.

The initial scope: load a Magellan database, build a CFG for a function, enumerate paths, compute dominance. No IDE integration, no background processes, no editing. Read-only analysis.


v1.0.0: 14 commands in three days (February 1–3)

The first stable release had 14 CLI commands, covering the core analyses:

Command What it does
status Database statistics
paths Enumerate all execution paths through a function
cfg Control-flow graph visualization (human, DOT, JSON)
dominators Dominator and post-dominator trees
loops Natural loop detection
unreachable Dead code detection (intra-function + uncalled functions via Magellan)
patterns Branching pattern detection (if/else, match)
frontiers Dominance frontier computation
verify Verify cached paths after code changes
blast-zone Path-based impact analysis
cycles Combined cycle detection (call graph + CFG)
slice Program slicing (backward/forward via Magellan call graph)
hotspots Risk scoring (path count + dominance + complexity)
icfg Inter-procedural CFG construction

Path storage used BLAKE3 content-addressed hashing for automatic deduplication. Paths are cached in SQLite tables (cfg_paths, cfg_path_elements) and automatically invalidated when the CFG changes (tracked by cfg_hash).

The crate name on crates.io is mirage-analyzermirage was already taken.


Iterative path enumeration (February 27)

v1.1.0 added a stack-based DFS implementation (enumerate_paths_iterative()) to prevent stack overflow on deeply nested CFGs. The recursive version works fine for shallow functions but blows the stack on functions with 20+ nesting levels.

The iterative version also does early path deduplication via BTreeSet – no duplicate paths stored. It produces identical results to the recursive version.

Both implementations exist because the recursive version is easier to verify against test cases, and the iterative version is safer in production.


The dual-backend experiment (February – May)

Like llmgrep, Mirage experimented with Magellan’s native binary backend. v1.0.2 added support for the retired-binary-backend format (.db files with B+Tree storage). v1.2.0 added geometric backend support (.geo files via geographdb-core).

Both backends had the same pattern as llmgrep: added complexity, persistence bugs, no measurable performance benefit for the query patterns in my stack.

The cleanup happened in two stages. v1.5.0 (May 26) deleted 1,487 lines of dead abstraction:

  • BackendRouter (1,111 LOC). An intermediate routing layer between CLI and storage. Over-engineered for a tool that talks to one database format. CLI uses MirageDb/Backend directly.
  • MIR/Charon extraction (376 LOC). The original approach for getting CFG data. Returned Ok(vec![]) on every real codebase. If MIR support ships, Magellan will index it – Mirage doesn’t need its own extraction pipeline.

v1.5.1 deleted another 708 lines:

  • Geometric backend (319 LOC storage + 389 LOC adapter). Feature-gated but never used in production.

Total: 2,195 lines removed. Three storage backends reduced to one (SQLite). The crate got smaller and faster.


The big fix release: v1.2.4 (April 27)

v1.2.4 fixed three interrelated problems that had been degrading analysis quality:

CFG edge loading. Mirage was reconstructing edges by guessing from terminator strings. The actual edges lived in Magellan’s cfg_edges table. Reading them directly made path enumeration return correct results, loop detection work, and dominator trees compute properly.

ICFG construction. The inter-procedural CFG (mirage icfg) couldn’t discover callees because GraphBackend::neighbors() returned empty. A two-pass algorithm fixed it: first pass builds all nodes, second pass adds inter-procedural edges after callee nodes exist.

Magellan v11 compatibility. The cfg_hash column replaced function_hash across all path enumeration, caching, and invalidation code. Without this, cached paths were never invalidated and path caching was broken.


The path enumeration bug that broke everything (May 6)

v1.3.0 fixed a critical issue: functions without explicit return statements were stored by Magellan as “fallthrough” terminators. Mirage’s find_exits() didn’t recognize these, so it returned empty exit sets. Result: 0 paths for ~90% of functions.

The fix added dead-end block fallback. When no exit blocks are found via terminators, blocks with no outgoing edges are treated as implicit exits. This single change took path enumeration from mostly-broken to working on real code.

This bug existed from day one. I didn’t catch it because I was testing against toy functions with explicit returns. Real Rust code uses implicit returns everywhere.


Intelligence features (May 26 – June 8)

Four releases added analysis beyond raw CFG queries:

v1.5.0 (May 26):

  • risk – Scores functions by cyclomatic complexity, path count, error path ratio, nesting depth, block count. Reports risk level (low/medium/high/critical).
  • suggest – Detects high complexity, deep nesting, excessive paths, dead code. Returns severity-tagged refactoring suggestions.
  • stats – Aggregate code statistics: function counts, block counts, path counts, complexity distribution, dead code blocks, coverage gaps.
  • diff – CFG diff across two separate Magellan databases (--before-db/--after-db) instead of fake snapshot IDs.
  • 21 production .unwrap() replaced with .expect("invariant: ...").
  • cli/mod.rs split from 6,725 lines into 22 command files and 9 test files.

v1.6.0 (May 28):

  • blast-zone --call-depth – Depth-aware inter-procedural analysis via Magellan’s SymbolNavigator. Limits call graph traversal to N hops.
  • Function resolution now uses SymbolNavigator for name-based resolution with disambiguation.

v1.7.0 (May 29):

  • forge module – High-level library API for programmatic access. Nine convenience functions (resolve_function, get_function_cfg, detect_cycles, find_dead_symbols, etc.) for agents and tools.

v1.8.0 (June 8):

  • --semantic-query flag on all function-targeted commands. Resolves natural-language queries to function entry points via HNSW embeddings (generated by Magellan, queried via Ollama). Falls back to exact --function name resolution.

The architecture that survived

Mirage’s architecture is simple because the complexity lives elsewhere:

Source files → Magellan (index) → .db file → Mirage (analyze) → JSON/DOT
                                              ↑
                                              forge module → agents, tools

Magellan owns indexing. Mirage owns analysis. The contract between them is a SQLite schema: graph_entities, cfg_blocks, cfg_edges, cfg_paths, cfg_dominators. Mirage reads this schema and produces analysis results.

The analysis pipeline for any command is:

  1. Resolve function name (SQL lookup or semantic query via HNSW)
  2. Load CFG blocks and edges from database
  3. Build petgraph DiGraph
  4. Run analysis (paths, dominance, loops, reachability, etc.)
  5. Output results (human, JSON, DOT)

No compiler, no language server, no background process. One query in, structured data out.


What I’d do differently

Test against real code earlier. The path enumeration bug that broke 90% of functions existed because I tested against toy code with explicit returns. I should have indexed Magellan’s own source from day one and verified every command against it.

Skip the MIR path entirely. Charon couldn’t handle real codebases. The AST-based approach worked immediately. I spent time building conversion logic that never produced useful data.

Skip the multi-backend abstraction. Three backends, two removed. The BackendRouter (1,111 LOC) was over-engineered for what amounts to “open a SQLite database.” Direct MirageDb/Backend calls are simpler and faster.


By the numbers

Metric Value
Lines of Rust 31,417
Published versions 25
CLI commands 22 (16 analysis + 6 utility)
Tests 474
Commits 354
Crates.io downloads 437
Dead code removed (v1.5.0 + v1.5.1) 2,195 LOC
Development span February – June 2026
Current version 1.8.0

Downloads are modest – 437 total across 25 versions. Same audience as llmgrep: me, my agents, and the Magellan ecosystem. This is infrastructure, not a consumer product.


The tool in context

Mirage answers questions that Magellan and llmgrep can’t:

  • “How many execution paths does this function have?” – mirage paths
  • “Which functions have the highest cyclomatic complexity?” – mirage hotspots
  • “What does a change to function X affect?” – mirage blast-zone
  • “Are there dependency cycles?” – mirage cycles
  • “Is there dead code?” – mirage unreachable
  • “What should I refactor?” – mirage suggest
  • “Find the function that handles errors in the parser” – mirage paths --semantic-query "error handling in the parser"

Magellan indexes. llmgrep queries symbols and references. Mirage analyzes control flow. Three tools, one database format, zero external services.

The code is at github.com/oldnordic/mirage. The crate is on crates.io. GPL-3.0.