Docs / design / cfg-design · Edit on GitHub

Control-Flow Graph (CFG) — Engineering Design

Per-function control-flow graphs: basic blocks, branch edges, and loop structure — the same IR layer compilers use before optimization.

CFG tab — blocks and Sigma graph (gbuilder)

Figure 1: CFG / PDG Analysis tab — function list, block table, and interactive CFG layout.


1. Goals

GoalHow
Visualize executable structureBasic blocks + conditional/unconditional edges
Feed PDG / slice / taintShared CFG construction in analysis pipeline
Scale to large reposInline JSON for small repos; archive_only lazy load for large
CLI dumpsinspect SYMBOL cfg (+ --prune, -f mermaid)

2. Architecture overview

flowchart LR
  subgraph build["discover --with-cfg"]
    PARSE[Language CFG builder]
    CFG[ControlFlowGraph]
    PARSE --> CFG
  end

  subgraph store["Storage"]
    ARC[cfg_pdg.archive.bin]
    PER[per-function storage]
    CFG --> ARC
    CFG --> PER
  end

  subgraph export["Dashboard"]
    CI[cfg_index.json]
    CD[cfg/*.json or archive sidecar]
    CV[CfgView.tsx + Sigma]
    CI --> CV
    CD --> CV
  end

Large repos: cfg_index.json sets detail_mode: "archive_only" — UI offers Load CFG graph to fetch one function from the record pack on demand.


3. CFG schema (dashboard detail)

FieldMeaning
blocks[]id, start_line, end_line, label
edges[]from, to, kind (true/false/unconditional)
entry / exitBlock ids

4. Rust implementation map

ComponentPath
CFG IRcrates/rgctl-analysis/src/cfg.rs
Language loweringcrates/rgctl-lang-*/ CFG hooks
Archivecrates/rgctl-analysis/src/cfg_pdg_archive.rs
CLI inspectsrc/cli/inspect.rs
Dashboard exportcrates/rgctl-dashboard/src/cfg_export.rs

5. Dashboard implementation

PiecePath
Tabdashboard/src/CfgView.tsx
Graph renderSigma.js with CFG_NODE_LEGEND / CFG_EDGE_LEGEND
Lazy loadloadCfgDetail(functionId) worker → archive records
Dominance tableImmediate dominators when detail includes idom

6. CLI usage

rgctl discover . --cfg
rgctl inspect MyClass#myMethod cfg
rgctl -f mermaid inspect MyClass#myMethod cfg --prune
rgctl -f json inspect MyClass#myMethod cfg -o /tmp/cfg.json

7. Testing

LayerLocation
CFG unit testscrates/rgctl-analysis/src/cfg.rs, cfg_builder.rs (test_go_*, test_java_*, test_rust_*, test_c_*, test_cpp_*)
Dashboard harnesstests/dashboard_harness.rs (cfg_index.json)
Playwrightdashboard/scripts/test-graph-tabs.mjs

Screenshots: capture-design-screenshots.mjsdocs/images/design/cfg/.

Go-specific notes

See go-language-coverage.md for the Tree-sitter Go checklist vs current lowering. Remaining honesty: go does not fork a parallel CFG; defer multiplicity inside loops is static-once.

Java-specific notes

Shared cfg_builder now lowers Java:

SurfaceLowering
if / && ||Unwrap parenthesized_expression; short-circuit via wire_condition
Classic forFields init / condition / update; continue → update block
enhanced_for_statementHeader branch + body cycle
switch_statement / switch_expressionswitch_block_statement_group (implicit fallthrough) and switch_rule (arrow, no fallthrough); return switch (...) visits nested switch
Labelsidentifier ':' stmt (no label field); labeled break/continue via breakable_stack
try / try_with_resourcesException edges from try entry and body statement blocks to catch; finally_stack unwind on return/throw; resources emit synthetic name.close() (reverse order) before user finally
throw_statementInside try → Exception to catch; otherwise terminal Exception exit

Rust-specific notes

SurfaceLowering
if / if let / && ||let_condition as branch; short-circuit via wire_condition; unwrap else_clause
match + guardsSequential arms; guard condition short-circuits to next arm; arm value field visited (returns lower)
try_expression (?)IfTrue success continue / IfFalse early Return exit
for pat in iterIterator expression + next/body cycle
loopUnconditional body cycle; exit only via break / return / panic (no IfFalse)
while / while letCondition header + body cycle
Labels'label: embedded on loop nodes; break/continue 'label via breakable_stack
panic! / todo! / unimplemented! / unreachable!macro_invocation → terminal Exception
.awaitBranch marker + basic-block resume split

Honesty left: implicit Drop cleanup blocks are not inserted; async_block / closure_expression are not separate sub-CFGs (nested control flow inside them is still walked when reachable).

C-specific notes

SurfaceLowering
if / && ||Unwrap parenthesized_expression and C++ condition_clause; short-circuit via wire_condition
forFields initializer / condition / update; continue → update
do / whileBody-then-condition cycle; condition on header block
switch / case_statementImplicit fallthrough between cases; default = case with no value
Ternaryconditional_expression → IfTrue/IfFalse merge
goto / labelsstatement_identifier label field; eager label blocks for forward jumps
abort / exit / _ExitTerminal Exception exit
setjmp / longjmpRecord setjmp sites; longjmp Jump back (intra-procedural approx)

Honesty left: computed goto *ptr not modeled; longjmp across functions is not inter-procedural; Duff’s-device case nesting relies on recursive case collection.

C++-specific notes

SurfaceLowering
C++17 if / switch initcondition_clause.initializer before value condition
condition_clauseUnwrap to value for short-circuit / branch text
for_range_loopRange expr + begin/end-style header cycle
try / catch / throwException edges; throw → catch or function exit
Ternary / goto / fallthroughSame as C
Coroutinesco_await / co_yield suspend split; co_return as return

Honesty left: RAII destructor cleanup blocks not inserted; overloaded &&/|| still short-circuit (no type info); lambda_expression is not a separate sub-CFG.

C#-specific notes

SurfaceLowering
if / && ||Short-circuit via wire_condition (same as Java/C)
forInit / condition / update; continue → update
foreachCollection expr + for-each header cycle (MoveNext-style)
switch_statement / switch_sectionCase fan-out; no implicit fallthrough; when guards before body
switch_expression / switch_expression_armSequential pattern arms (C#); Java switch_expression falls back to statement lowering
?? / ?.Null-coalesce and null-conditional branch splits
try / catch / when / finallyException edges; catch filter before body; finally on exit
using / lockFinally-style Dispose() / Monitor.Exit on all exits (statement and using var declaration)
awaitAsync state-machine split: IfTrue resume / IfFalse suspend exit
yield return / yield breakSuspend split / terminal exit through finallies
goto / labelsUnconditional jump within method
goto case / goto defaultResolve to pre-created switch section entry blocks
Lambdas / local functionsDisconnected sub-CFG (Jump from definition); body returns use nested exit

Honesty left: full async state-machine (multiple awaits / MoveNext resume table) is still a per-await bifurcate, not a global state enum; expression-bodied members and iterators beyond yield are not specialized further.


8. Related docs