Review an LFortran Pull Request
Find correctness and design problems before style issues. The central question for every backend change is:
What decision is the backend making, and can the compiler make that decision earlier and represent it explicitly in ASR?
Read AGENTS.md first. Read references/review-rules.md
for the detailed principles. Consult references/examples.md
when a concrete comparison would help.
Gather the change
For a GitHub PR, collect the description, commits, changed files, and diff:
gh pr view <number> --repo lfortran/lfortran \
--json title,body,author,baseRefName,headRefName,commits,files,reviews
gh pr diff <number> --repo lfortran/lfortran
For a local branch, inspect the merge-base diff and the worktree. Do not review only the latest commit when the PR contains several commits.
Identify:
- The bug or feature and its MRE.
- The first compiler phase where behavior becomes wrong.
- The invariant the fix should establish.
- The tests that prove the old behavior fails and the new behavior works.
Check that the description's rationale and every helper, artifact, or behavior it names are present and accurate in the diff. Treat architectural claims as hypotheses until the code and reproducer support them.
If these cannot be determined from the PR, report that as a review finding rather than guessing.
Review placement from earliest to latest
Walk this ladder in order. Stop at the earliest layer that can express the decision cleanly:
- Parser / AST: syntax and source-form distinctions. Parser actions should produce AST even for semantically invalid input; semantic diagnostics belong in AST-to-ASR.
- AST-to-ASR: symbol resolution, type coercion, casts, overload selection, semantic diagnostics, and other source-language decisions.
- ASR representation: add or use a node field, enum, structured type, or
other explicit state when downstream code needs to know a semantic fact.
Extend ASR verification so malformed states fail near their origin. Only
symbol nodes own symbol tables; represent statement-local scopes with a
Blockor a dedicated symbol node. A new ASR node also needs minimal support in every exhaustive visitor, including verification, round-trip printing, and dependency collection. - Existing ASR pass: use or extend a pass when the decision requires a whole-tree transformation or canonical lowering.
- New ASR pass: consider one only when it localizes a coherent algorithm, simplifies multiple consumers, produces a useful explicit IR, and has clear ordering and invariants.
- Backend: keep only target-specific mechanics that cannot be represented earlier.
Do not accept an LLVM-type query, opportunistic bitcast, magic string, or backend-only type inference as a substitute for understanding the ASR. Querying LLVM for a type and casting based on the result is a warning that ASR does not yet express what is happening.
Decide whether to add an ASR pass
An ASR pass is justified when most of these are true:
- The transformation requires non-local knowledge or repeated tree rewriting.
- Several backends or later phases benefit from one canonical form.
- The output invariant is explicit and verifiable.
- Centralizing the algorithm removes duplicated complex logic.
- The pass can be enabled, disabled, ordered, and tested independently.
Prefer an ASR field plus verifier, an existing pass, or direct AST-to-ASR construction when the transformation is local. A new pass adds compile time, ordering dependencies, maintenance, and another failure boundary; LFortran should have a few strong passes rather than many narrow ones.
If the work must remain in a backend, require the smallest target-specific
change possible. Reuse asr_to_llvm, llvm_utils, ASRUtils, PassUtils, and
existing lowering helpers. Reject duplicated loop builders, copied symbol
lookup, hard-coded argument positions, and long one-off blobs.
Review correctness and invariants
- Trace types, ranks, kinds, ownership, physical representations, and ABI facts from ASR to the changed code.
- Check scalar and array forms, sections, allocatable/pointer forms, inherited types, type-bound procedures, nondefault kinds, and target differences when relevant.
- Preserve language semantics, not merely contiguous memory behavior. For example, character-array elements used as an internal file are separate records; flattening them into one byte stream is wrong.
- Prefer explicit structured state over string inspection and positional assumptions.
- Identify intrinsic types and modules by structured origin, not a spoofable name string.
- Distinguish compiler-synthesized and user-visible entities with explicit ASR provenance, not generated-name prefixes or node-kind guesses.
- Represent defined-layout properties such as
SEQUENCEandBIND(C)in ASR; use that state for physical layout and interoperability decisions. - Preserve symbol-table invariants: imported symbols point to their original definitions, and symbol creation does not duplicate an existing local name.
- Use assertions that test the actual boolean invariant, or ASR verification, for impossible states instead of quietly accepting them through defensive conditionals.
- Propagate failures with
Resultand diagnostics carrying sourceLocation; do not introduce exceptions or broad try/catch recovery. - Point diagnostics at the precise offending tokens and render types as valid Fortran syntax.
- When accepting a deliberate nonstandard extension, emit a located portability warning that explains the conforming alternative.
- In runtime C, pass actual buffer capacities to bounded APIs and avoid undefined conversions or writes.
- Ensure
libasrremains frontend-independent.
Review reuse and scope
Search before accepting a new helper or algorithm:
rg "<related symbol or operation>" src integration_tests tests
Ask whether the patch:
- duplicates a scalar path for arrays instead of normalizing the type once;
- adds a core utility when a pass-layer helper already exists;
- reconstructs loop nests, bounds, or array references already handled by a shared helper;
- mixes a bug fix with refactoring, formatting, generated-output churn, or an unrelated cleanup;
- adds comments that repeat the code or describe behavior not implemented; or
- introduces unused, dead, or unreachable code.
Recommend the concrete existing helper or layer, not just "simplify this."
Review tests
Require a test that fails without the fix and passes with it.
- Prefer a compact registered integration test with at least
gfortranandllvmlabels when the behavior can run end to end. - Verify that CI executes the exact binary, backend, options, and code path changed by the PR.
- Add a targeted edge case for each newly handled error or semantic branch.
- Put recoverable semantic diagnostics in a complete continue-compilation test and verify each error branch independently.
- Keep shared diagnostic fixtures append-only so new cases do not shift existing source locations.
- Test portability warnings themselves, not only the accepted runtime behavior; document why unsupported reference compilers are omitted from test labels.
- Differential C interoperability tests must include each compiler's own
ISO_Fortran_binding.hand use its corresponding runtime. - Keep fixtures proportional to the bug. A 1,300-line program for one out-of-bounds error is not an acceptable regression test.
- Avoid large reference-output churn. Make preparatory spacing or mechanical changes separately so the behavioral diff remains reviewable. Regeneration caused directly by an ASR schema change is legitimate, but should be mechanically consistent.
- For external-project CI failures, identify the failing toolchain and root cause before accepting a skip or an upstream patch that disables coverage.
- Build changes must track real inputs, quote paths, fail early when required artifacts are absent, and avoid committed binary or large generated blobs.
- Any CI build or generation step that can fail must run on pull requests unless it inherently requires main-only publishing credentials.
Account for pragmatic follow-ups
An approval can mean "safe enough to merge and iterate," not that every line is endorsed. Keep unresolved correctness issues visible. Classify each finding as:
- Blocker: wrong semantics, invalid ASR, regression, unsafe error handling, or an untested core behavior.
- Rework: maintainability or placement problem that should be fixed in this PR because it shapes the implementation.
- Follow-up: bounded debt that does not invalidate the current change; state the exact follow-up test or refactor.
Do not silently downgrade a correctness bug because another review approved the PR.
A draft, pending, or otherwise unsubmitted review does not gate merging. Confirm that any blocker-level concern was resolved or explicitly tracked before treating a merge as evidence that the implementation is sound. Treat approval conditioned on a fresh full run as incomplete until that run succeeds.
Write the review
Report only actionable findings, ordered by severity. Use this shape:
[blocker|rework|follow-up] Short title
path/to/file.cpp:<line>
What is wrong and the concrete input or invariant that demonstrates it.
Why the current layer or implementation is risky.
Recommended shape: the earlier phase, ASR state/pass, or existing helper to use.
Then add a short Architecture assessment:
- earliest wrong phase;
- chosen fix layer and whether it is justified;
- ASR invariant added or still missing;
- backend complexity added or removed.
If no actionable findings remain, say so and mention only meaningful residual risks or test gaps. Treat automated-review findings as hypotheses: reproduce and understand them before reporting them as blockers. Do not manufacture style comments.