Skip to content

Commit 38a7b4b

Browse files
sbryngelsonclaude
andauthored
Add Fortran/Fypp static analysis linter (#1193)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1343d93 commit 38a7b4b

28 files changed

Lines changed: 455 additions & 357 deletions

.claude/rules/fortran-conventions.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,28 @@ Every Fortran module follows this pattern:
2323

2424
## Forbidden Patterns
2525

26-
Caught by `./mfc.sh precheck` (source lint step 4/5):
26+
All checks below are enforced by `python3 toolchain/mfc/lint_source.py`
27+
(runs via `./mfc.sh precheck` and CI). See that file for the full list.
28+
29+
Fortran/Fypp source (`src/`):
2730
- `dsqrt`, `dexp`, `dlog`, `dble`, `dabs`, `dcos`, `dsin`, `dtan`, etc. → use generic intrinsics
2831
- `1.0d0`, `2.5d-3` (Fortran `d` exponent literals) → use `1.0_wp`, `2.5e-3_wp`
2932
- `double precision` → use `real(wp)` or `real(stp)`
3033
- `real(8)`, `real(4)` → use `wp` or `stp` kind parameters
3134
- Raw `!$acc` or `!$omp` directives → use Fypp GPU_* macros from `parallel_macros.fpp`
32-
- Full list of forbidden patterns: `toolchain/bootstrap/precheck.sh`
35+
- `int(8._wp, ...)` hardcoded byte size → use `storage_size(0._stp)/8`
36+
- Bare integer kind like `2_wp` → use `2.0_wp`
37+
- Junk patterns (`...`, `---`, `===`) in code or comments (no separator comments)
38+
- Duplicate entries in Fypp `#:for ... in [...]` lists
39+
- Identical adjacent non-trivial lines (copy-paste bugs)
40+
41+
Python (`examples/`, `benchmarks/`, `toolchain/`):
42+
- `===` separator comments → remove
43+
- `----` or longer separator comments → remove (3 dashes `---` is allowed for markdown)
44+
45+
Shell (`.github/`, `toolchain/`):
46+
- `===` or `----` separator comments → remove
47+
- Echo separators longer than 20 characters → shorten
3348

3449
Enforced by convention/code review (not automated):
3550
- `goto`, `COMMON` blocks, global `save` variables

.github/scripts/run_case_optimization.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ for case in "${benchmarks[@]}"; do
3636
case_dir="$(dirname "$case")"
3737
case_name="$(basename "$case_dir")"
3838
echo ""
39-
echo "========================================"
39+
echo "===================="
4040
echo "Case-optimization test: $case_name"
41-
echo "========================================"
41+
echo "===================="
4242

4343
# Clean any previous output
4444
rm -rf "$case_dir/D" "$case_dir/p_all" "$case_dir/restart_data"
@@ -65,11 +65,11 @@ for case in "${benchmarks[@]}"; do
6565
done
6666

6767
echo ""
68-
echo "========================================"
68+
echo "===================="
6969
echo "Case-optimization summary: $passed passed, $failed failed"
7070
if [ $failed -gt 0 ]; then
7171
echo "Failed cases:$failed_cases"
7272
fi
73-
echo "========================================"
73+
echo "===================="
7474

7575
[ $failed -eq 0 ] && exit 0 || exit 1

.github/scripts/run_parallel_benchmarks.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ cluster="$3"
2020
# Get the directory where this script lives (pr/.github/scripts/)
2121
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2222

23-
echo "=========================================="
23+
echo "===================="
2424
echo "Starting benchmark jobs..."
25-
echo "=========================================="
25+
echo "===================="
2626

2727
# For Phoenix GPU benchmarks, select a consistent GPU partition so PR and
2828
# master always land on the same GPU type.
@@ -82,9 +82,9 @@ if [ "${pr_exit}" -ne 0 ] || [ "${master_exit}" -ne 0 ]; then
8282
echo "WARNING: Benchmark jobs had failures: pr=${pr_exit}, master=${master_exit}"
8383
echo "Checking for partial results..."
8484
else
85-
echo "=========================================="
85+
echo "===================="
8686
echo "Both benchmark jobs completed successfully!"
87-
echo "=========================================="
87+
echo "===================="
8888
fi
8989

9090
pr_yaml="pr/${job_slug}.yaml"

.github/scripts/submit_and_monitor_bench.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ if [ ! -f "$yaml_file" ]; then
3636
echo ""
3737
output_file="${job_slug}.out"
3838
echo "[$dir] Last 100 lines of job output ($output_file):"
39-
echo "----------------------------------------"
39+
echo "--------------------"
4040
tail -n 100 "$output_file" 2>/dev/null || echo " Could not read output file"
41-
echo "----------------------------------------"
41+
echo "--------------------"
4242
exit 1
4343
fi
4444

.github/workflows/cleanliness.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ jobs:
8686
grep -F 'Wmaybe-uninitialized' master.txt > mMaybe.txt
8787
diff prMaybe.txt mMaybe.txt
8888
89+
- name: Conversion Warnings Diff
90+
continue-on-error: true
91+
run: |
92+
grep -F 'Wconversion' pr.txt > prConversion.txt
93+
grep -F 'Wconversion' master.txt > mConversion.txt
94+
diff prConversion.txt mConversion.txt
8995
9096
- name: Everything Diff
9197
continue-on-error: true
@@ -106,12 +112,14 @@ jobs:
106112
pr_argument=$(grep -c -F 'Wunused-dummy-argument' pr.txt)
107113
pr_value=$(grep -c -F 'Wunused-value' pr.txt)
108114
pr_uninit=$(grep -c -F 'Wmaybe-uninitialized' pr.txt)
115+
pr_conversion=$(grep -c -F 'Wconversion' pr.txt)
109116
pr_everything=$(grep -c '\-W' pr.txt)
110117
111118
master_variable=$(grep -c -F 'Wunused-variable' master.txt)
112119
master_argument=$(grep -c -F 'Wunused-dummy-argument' master.txt)
113120
master_value=$(grep -c -F 'Wunused-value' master.txt)
114121
master_uninit=$(grep -c -F 'Wmaybe-uninitialized' master.txt)
122+
master_conversion=$(grep -c -F 'Wconversion' master.txt)
115123
master_everything=$(grep -c '\-W' master.txt )
116124
117125
echo "pr_everything=$pr_everything" >> $GITHUB_ENV
@@ -124,6 +132,7 @@ jobs:
124132
echo "Unused Dummy Argument: $pr_argument, Difference: $((pr_argument - master_argument))"
125133
echo "Unused Value: $pr_value, Difference: $((pr_value - master_value))"
126134
echo "Maybe Uninitialized: $pr_uninit, Difference: $((pr_uninit - master_uninit))"
135+
echo "Conversion: $pr_conversion, Difference: $((pr_conversion - master_conversion))"
127136
echo "Everything: $pr_everything, Difference: $((pr_everything - master_everything))"
128137
129138

.github/workflows/lint-source.yml

Lines changed: 0 additions & 57 deletions
This file was deleted.

.github/workflows/lint-toolchain.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ jobs:
1717
- name: Initialize MFC
1818
run: ./mfc.sh init
1919

20-
- name: Lint and test the toolchain
21-
run: ./mfc.sh lint
20+
- name: Precheck (format, spelling, lint, source lint, docs)
21+
run: ./mfc.sh precheck -j $(nproc)
2222

2323
- name: Generate toolchain files
2424
run: ./mfc.sh generate

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ All commands run from the repo root via `./mfc.sh`.
3939
./mfc.sh test --generate --only <feature> # Regenerate golden files after intentional output change
4040

4141
# Verification (pre-commit CI checks)
42-
./mfc.sh precheck -j 8 # Run all 5 lint checks (same as CI gate)
42+
./mfc.sh precheck -j 8 # Run all 6 lint checks (same as CI gate)
4343
./mfc.sh format -j 8 # Auto-format Fortran (.fpp/.f90) + Python
4444
./mfc.sh lint # Ruff lint + Python unit tests
4545
./mfc.sh spelling # Spell check

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ if (CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
150150
-fsignaling-nans
151151
-finit-real=snan
152152
-finit-integer=-99999999
153+
-Wconversion
153154
-Wintrinsic-shadow
154155
-Wunderflow
155156
-Wrealloc-lhs

examples/nD_perfect_reactor/analyze.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@
2929
matplotlib.use("Agg")
3030

3131

32-
# ---------------------------------------------------------------------------
3332
# Configuration
34-
# ---------------------------------------------------------------------------
3533
CASE_DIR = "."
3634
Y_MAJORS = {"H", "O", "OH", "HO2"}
3735
Y_MINORS = {"H2O", "H2O2"}
@@ -40,9 +38,7 @@
4038
skinner_induction_time = 0.052e-3 # Skinner & Ringrose (1965)
4139

4240

43-
# ---------------------------------------------------------------------------
4441
# Load MFC output
45-
# ---------------------------------------------------------------------------
4642
steps = discover_timesteps(CASE_DIR, "silo")
4743
if not steps:
4844
sys.exit("No silo timesteps found — did you run post_process?")
@@ -61,9 +57,7 @@
6157
for y in Y_VARS:
6258
mfc_Ys[y].append(float(assembled.variables[f"Y_{y}"][mid]))
6359

64-
# ---------------------------------------------------------------------------
6560
# Cantera 0-D reference
66-
# ---------------------------------------------------------------------------
6761
time_save = Tend / SAVE_COUNT
6862

6963

@@ -86,9 +80,7 @@ def generate_ct_saves():
8680

8781
ct_ts, ct_Ys, ct_rhos = generate_ct_saves()
8882

89-
# ---------------------------------------------------------------------------
9083
# Induction time: first step where [OH] molar concentration >= 1e-6 mol/m^3
91-
# ---------------------------------------------------------------------------
9284

9385

9486
def find_induction_time(ts, Ys_OH, rhos):
@@ -106,9 +98,7 @@ def find_induction_time(ts, Ys_OH, rhos):
10698
print(f" Cantera: {ct_induction:.3e} s" if ct_induction is not None else " Cantera: not reached")
10799
print(f" (Che)MFC: {mfc_induction:.3e} s" if mfc_induction is not None else " (Che)MFC: not reached")
108100

109-
# ---------------------------------------------------------------------------
110101
# Plot
111-
# ---------------------------------------------------------------------------
112102
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
113103
_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
114104
_color = {y: _colors[i % len(_colors)] for i, y in enumerate(sorted(Y_VARS))}

0 commit comments

Comments
 (0)