Compiler / Complexity Analysis API

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 (Classic or Modified),
  • an evidence_collected flag that distinguishes omitted evidence from a body with no decisions,
  • deterministic source-ordered BodyReport values,
  • and an AnalysisSummary covering the module.

Each BodyReport provides:

  • a stable BodyId made from body kind and source token offset,
  • body kind (Function, Method, AnonymousFunction, Test, or NestedTest), 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 Decision evidence when collect_decisions is 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_complexity strictly 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 L5001 body-level diagnostic.
  • Editor tools may use BodyId and 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

Quick links

Type to search across the site. Use the up and down arrow keys to choose a result, Enter to open it, and Escape to close.