Complexity Analysis API
src/complexity.zig is the canonical cyclomatic-complexity implementation for
Silk compiler tooling. silk check, silk-lsp, future reports, and repository
tools consume this module rather than maintaining separate counters.
Downstream Zig tools that import the installed silk module use
silk.tooling.complexity. The matching silk.tooling.parser and
silk.tooling.ast exports provide the parsed module accepted by the analyzer.
These exports point at the compiler's canonical modules; they are not parallel
wrapper types.
The analyzer operates on a parsed ast.Module. It does not require name
resolution or type checking, although target-aware callers should run attribute
filtering first so inactive declarations do not appear in the report.
Public contract#
The primary entry point is:
const silk = @import("silk");
var parser = try silk.tooling.parser.Parser.init(allocator, source);
const module = try parser.parseModule();
var analysis = try silk.tooling.complexity.analyzeModule(allocator, &module, .{
.variant = .Classic,
.collect_decisions = true,
});
defer analysis.deinit(allocator);
Analysis has schema version 1 and contains:
- the selected
Variant(ClassicorModified), - an
evidence_collectedflag that distinguishes omitted evidence from a body with no decisions, - deterministic source-ordered
BodyReportvalues, - and an
AnalysisSummarycovering the module.
Each BodyReport provides:
- a stable
BodyIdmade from body kind and source token offset, - body kind (
Function,Method,AnonymousFunction,Test, orNestedTest), borrowed name, nearest scope/owner name, and optional parent body identity, - the exact declaration/body anchor token,
BodyMetrics, including cyclomatic complexity, contributed decision points, deepest decision nesting, decision-event count, and saturation state,- source-ordered
Decisionevidence whencollect_decisionsis enabled, - category-count helpers, threshold comparison, and allocation-backed display name/detail/breakdown renderers shared by diagnostics.
Each Decision records its DecisionKind, category, contribution, one-based
control-flow nesting depth, source token, and AnchorPrecision. Each decision
kind also exposes a stable short label and a user-facing explanation of why the
syntax creates another selectable runtime path, allowing evidence views and
editor hovers to remain consistent. Exact means the token is the syntax that
introduced the decision. Related is used only when the AST has no distinct
token for the syntactic separator, such as a parameter default's = or a
chained if let clause from an older/synthetic AST producer; the related token
still identifies the relevant operand or parameter.
AnalysisSummary reports body count, total complexity, total decision points,
total decision events, maximum complexity, deepest nesting,
highest-complexity body index, and the number of saturated bodies. Empty
modules have zero totals and no highest-body index.
The report owns its body and decision slices. Names and tokens borrow storage
from the AST, so the parsed module must outlive the report. Analysis.deinit
releases only report-owned allocations. Callers using an arena may omit
individual cleanup and release the arena as one unit.
Analysis.validate() performs an allocation-free integrity check for tooling
and serialization boundaries. It verifies the schema, source ordering, unique
body IDs, identity/token agreement, parent metadata, summary, baseline
equation, category totals, and, when retained, decision ordering and
contribution evidence. BodyId is module-local; a workspace tool pairs it with
the module URI or canonical path. Analyzer-produced reports satisfy this
contract; the method is also useful after transport or adapter code has copied
the public data model.
measureModule remains as a compact compatibility helper for callers that need
only the former name / token / value findings. New tooling should use
analyzeModule so it does not discard evidence.
Mathematical invariants#
For every body that does not saturate:
cyclomatic_complexity = 1 + decision_points
decision_points = sum(decision.contribution) when evidence is collected
Every runtime body starts at 1. Arithmetic saturates instead of wrapping;
saturated = true makes that state explicit to callers. Module totals use
saturating u64 arithmetic and expose a summary saturation flag.
Bodies and their decision evidence are sorted by byte offset with stable kind-based tie breaking. This makes reports deterministic across allocator choices and traversal details. Nested anonymous functions and nested tests are independent bodies: their decisions never inflate the enclosing body.
Decision taxonomy#
The analyzer groups concrete decision kinds into categories so callers can show both exact evidence and compact summaries.
| Category | Decision kinds |
|---|---|
| Conditional | if, if let, let ... else, and if expressions |
| Loop | while, while let, for, C-style for, loop, async loop, and task loop |
| Pattern | additional if let / while let chain clauses and the refutable filter in for let |
| Match | classic non-catch-all arms or one modified-mode match event |
| Logical | short-circuit &&, ` |
| Optional/error | optional access ?., error propagation ?, and terminal coalescing control |
| Parameter | a defaulted runtime parameter |
for let contributes one loop decision and one pattern-filter decision. A
typed match binder such as value: SomeType is selective and contributes in
classic mode. Only _ and an untyped whole-value statement binder are
catch-all patterns.
For Classic, every non-catch-all match arm contributes one decision. For
Modified, the entire match contributes one decision regardless of its arms.
The evidence token is the pattern token in classic mode and the match token
in modified mode.
Formal Silk specifications and expressions evaluated only for compile-time
metadata (sizeof, alignof, typename, struct defaults, and specification
statements) do not add runtime decisions. Executable anonymous functions found
inside such expressions are still reported as independent bodies.
Consumer guidance#
- Threshold rules compare
BodyMetrics.cyclomatic_complexitystrictly greater than the configured maximum. - Diagnostics should anchor the body token and use the shared display/detail
helpers. Decision evidence is supporting data, not a replacement for the
stable
L5001body-level diagnostic. - Editor tools may use
BodyIdand decision tokens for code lenses, hovers, heat maps, or navigation without parsing human-readable diagnostic text. - Machine consumers should key serialized data by
schema_version, enum name, body ID, and byte offsets. Human-rendered strings are intentionally polished but are not a machine protocol.
The package rule and CLI/LSP behavior are specified in package manifests, cli silk, and lsp silk. Stable user diagnostics are specified in diagnostics.
Source repository · Edit this page · View Markdown