Coverage in NanoLang is a build-system-level tool rather than a NanoLang module — there is no import statement required. When you compile with coverage instrumentation enabled, the NanoLang compiler generates C code annotated with gcov/llvm-cov counters. After running the shadow tests, you read the resulting coverage data to see which lines were hit.
How Coverage Works in NanoLang
The compilation pipeline is:
.nano source
↓ nanoc (with --coverage flag, if supported)
.c source (with __gcov_* instrumentation)
↓ clang/gcc (with -fprofile-arcs -ftest-coverage)
.o + .gcno files
↓ run the binary (shadow tests execute here)
.gcda files
↓ gcov or llvm-cov report
coverage report
Because shadow tests run at compile time (before your program's main executes), the instrumented binary is run as part of the build, and coverage data is collected automatically.
Running Tests with Coverage
Coverage is typically enabled through the project Makefile. A common workflow:
# Clean previous coverage data
make clean-coverage
# Build and run shadow tests with coverage instrumentation
make test-coverage
# Generate the HTML report
make coverage-report
# Open the report
open coverage/index.html
If your project does not yet have coverage targets, you can add them manually. The essential compiler flags are:
# Compile with coverage
clang -fprofile-arcs -ftest-coverage -o myprogram myprogram.c
# Run the program (shadow tests fire automatically)
./myprogram
# Generate gcov report
gcov myprogram.c
# Or use llvm-cov for prettier output
llvm-cov gcov myprogram.c
Reading the Coverage Report
A gcov report for a NanoLang file looks like this:
-: 0:Source:myfile.nano.c
-: 1:/* Generated by nanoc */
-: 2:#include "nanolang_runtime.h"
100: 5:int nl_clamp(int value, int lo, int hi) {
100: 6: if (value < lo) return lo;
42: 7: if (value > hi) return hi;
100: 8: return value;
-: 9:}
#####: 11:int nl_unused_function(int x) {
#####: 12: return x * 2;
-: 13:}
The left column shows:
-— non-executable line (comments, braces, declarations)- A **count** — the line was executed that many times
#####— the line was **never executed** (this is what you want to eliminate)
What Good Coverage Looks Like
For a module with thorough shadow tests you should expect:
- **Function coverage:** 100% — every function should have a shadow test
- **Line coverage:** 90–100% — most lines should be executed by at least one test
- **Branch coverage:** 80–100% — both sides of
if/elseshould be tested
The lines most likely to be uncovered are:
- Error handling paths (e.g. "this should never happen" branches)
- Defensive
elsebranches required by NanoLang syntax but never taken - Code that is only reachable from
mainor other I/O-driven paths
Improving Coverage
If coverage shows uncovered lines, the fix is usually to add more assertions to the relevant shadow test:
**Before (low coverage):**
fn grade(score: int) -> string {
if (>= score 90) { return "A" }
if (>= score 80) { return "B" }
if (>= score 70) { return "C" }
return "F"
}
shadow grade {
assert (== (grade 95) "A") # Only tests the "A" path
}
**After (full coverage):**
shadow grade {
assert (== (grade 95) "A")
assert (== (grade 85) "B")
assert (== (grade 75) "C")
assert (== (grade 60) "F")
assert (== (grade 90) "A") # boundary
assert (== (grade 80) "B") # boundary
assert (== (grade 70) "C") # boundary
}
Coverage and the Required else Branch
NanoLang requires else clauses in certain positions. Sometimes the else branch is a semantic no-op:
if (some_condition) {
(do_something)
} else {
(print "") # required no-op
}
Coverage tools will report this (print "") as uncovered if some_condition is always true in your tests. This is acceptable — document it with a comment so reviewers understand it is intentional:
} else {
(print "") # required no-op: condition always true in test scenarios
}
Branch Coverage vs Line Coverage
Line coverage tells you whether a line was reached. Branch coverage tells you whether both the true and false branches of every condition were taken. In NanoLang, achieving full branch coverage typically requires:
1. Testing every if condition with an input that makes it true
2. Testing every if condition with an input that makes it false
3. Testing while loops with zero iterations (loop never entered) and at least one iteration
fn sum_positive(arr: array<int>) -> int {
let mut total: int = 0
let mut i: int = 0
while (< i (array_length arr)) {
let v: int = (array_get arr i)
if (> v 0) {
set total (+ total v)
} else {
(print "") # skip negatives
}
set i (+ i 1)
}
return total
}
shadow sum_positive {
# Empty array: while loop never entered
assert (== (sum_positive []) 0)
# Array with only positives: else branch never taken
assert (== (sum_positive [1, 2, 3]) 6)
# Array with mixed: both branches taken
assert (== (sum_positive [1, -2, 3, -4]) 4)
# Array with only negatives: if branch never taken
assert (== (sum_positive [-1, -2]) 0)
}
Coverage with Property Tests
Property tests (from the proptest module) also contribute to coverage. When proptest generates 100 random inputs and runs your property function, every executed line in the called functions is counted. This means a well-written property test can drive up branch coverage significantly without requiring you to enumerate all branches manually.
from "modules/proptest/proptest.nano" import forall_int, int_range, prop_pass,
report_passed, PropertyReport
fn prop_sum_positive_nonneg(seed: int) -> string {
let arr: array<int> = [seed, (- 0 seed), (+ seed 1)]
let result: int = (sum_positive arr)
if (>= result 0) {
return (prop_pass)
} else {
return (prop_fail "negative sum")
}
}
shadow prop_sum_positive_nonneg {
let report: PropertyReport = (forall_int "sum_positive_nonneg"
(int_range -50 50)
prop_sum_positive_nonneg)
assert (report_passed report)
}
Coverage Gaps to Investigate
Some coverage gaps are worth investigating rather than ignoring:
- **An entire function is uncovered** — its shadow test is missing or broken
- **A large block is uncovered** — there may be a logic error making the block unreachable
- **Only one branch of a critical condition is tested** — the other branch may contain a bug
Some gaps are acceptable:
- No-op
elsebranches required by the language mainfunction bodies (often not meaningful to unit-test)- Logging and diagnostic output paths
---
**Previous:** 20.1 proptest
**Next:** 20.3 Best Practices